pyeapi-1.0.2/0000755000076500000240000000000014447406213014144 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/.coveragerc0000644000076500000240000000122014447405743016267 0ustar dlyssenkostaff00000000000000# .coveragerc to control coverage.py [run] branch = True omit = *mock* *test* *netaddr* *site-packages* [report] # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: show_missing = True ignore_errors = True [html] directory = coverage_html_report pyeapi-1.0.2/CHANGELOG.md0000644000076500000240000000024414447405743015764 0ustar dlyssenkostaff00000000000000Python Client for eAPI ====================== Full [release notes] [rns] hosted at readthedocs [rns]: http://pyeapi.readthedocs.org/en/master/release-notes.html pyeapi-1.0.2/LICENSE0000644000076500000240000000272014447405743015161 0ustar dlyssenkostaff00000000000000Copyright (c) 2016, Arista Networks EOS+ All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Arista nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pyeapi-1.0.2/MANIFEST.in0000644000076500000240000000132414447405743015711 0ustar dlyssenkostaff00000000000000include *.md include *.txt include .coveragerc include LICENSE include Makefile include VERSION recursive-include examples *.conf recursive-include examples *.py recursive-include test *.conf recursive-include test *.json recursive-include test *.portchannel recursive-include test *.py recursive-include test *.text recursive-include test *.vxlan recursive-include test *.bgp recursive-include test *.ospf recursive-include test *.routemaps recursive-include test *.varp recursive-include test *.varp_null recursive-include test *.vrf recursive-include test *.vrrp recursive-include test *.yaml recursive-include docs description.rst recursive-include docs *.py recursive-include docs *.rst recursive-include docs Makefile pyeapi-1.0.2/Makefile0000644000076500000240000000341614447405743015617 0ustar dlyssenkostaff00000000000000#!/usr/bin/make # WARN: gmake syntax ######################################################## # Makefile for pyeapi # # useful targets: # make sdist -- build python source distribution # make pep8 -- pep8 checks # make pyflakes -- pyflakes checks # make flake8 -- flake8 checks # make check -- manifest checks # make tests -- run all of the tests # make unittest -- runs the unit tests # make systest -- runs the system tests # make clean -- clean distutils # make coverage_report -- code coverage report # ######################################################## # variable section NAME = "pyeapi" PYTHON=python COVERAGE=coverage SITELIB = $(shell $(PYTHON) -c "from distutils.sysconfig import get_python_lib; print get_python_lib()") VERSION := $(shell cat VERSION) ######################################################## all: clean check pep8 flake8 tests pep8: pycodestyle -r --ignore=E402,E731,E501,E221,W291,W391,E302,E251,E203,W293,E231,E303,E201,E202,E225,E261,E241,E128 pyeapi/ test/ pyflakes: pyflakes pyeapi/ test/ flake8: flake8 --ignore=E128,E201,E202,E302,E303,E402,E731,W391 --exit-zero pyeapi/ flake8 --ignore=E128,E201,E202,E302,E303,E402,E731,W391,N802 --max-line-length=100 test/ check: check-manifest clean: @echo "Cleaning up distutils stuff" rm -rf build rm -rf dist rm -rf MANIFEST rm -rf *.egg-info @echo "Cleaning up byte compiled python stuff" find . -type f -regex ".*\.py[co]$$" -delete @echo "Cleaning up doc builds" rm -rf docs/_build rm -rf docs/api_modules rm -rf docs/client_modules sdist: clean $(PYTHON) setup.py sdist tests: unittest systest unittest: clean $(COVERAGE) run -m unittest discover test/unit -v systest: clean $(COVERAGE) run -m unittest discover test/system -v coverage_report: $(COVERAGE) report --rcfile=".coveragerc" pyeapi-1.0.2/PKG-INFO0000644000076500000240000000306414447406213015244 0ustar dlyssenkostaff00000000000000Metadata-Version: 2.1 Name: pyeapi Version: 1.0.2 Summary: Python Client for eAPI Home-page: https://github.com/arista-eosplus/pyeapi Author: Arista EOS+ CS Author-email: eosplus-dev@arista.com License: BSD-3 Description: The Python Client for eAPI ========================== The Python Client for eAPI (pyeapi) is a native Python library wrapper around Arista EOS eAPI. It provides a set of Python language bindings for configuring Arista EOS nodes. The Python library can be used to communicate with EOS either locally (on-box) or remotely (off-box). It uses a standard INI-style configuration file to specify one or more nodes and connection profiles. The pyeapi library also provides an API layer for building native Python objects to interact with the destination nodes. The API layer is a convenient implementation for working with the EOS configuration and is extensible for developing custom implementations. This library is freely provided to the open source community for building robust applications using Arista EOS. Support is provided as best effort through Github issues. Keywords: networking arista eos eapi Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: System Administrators Classifier: Topic :: System :: Networking Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 3 :: Only Provides-Extra: dev Provides-Extra: test pyeapi-1.0.2/README.md0000644000076500000240000000701414447405743015434 0ustar dlyssenkostaff00000000000000# Arista eAPI Python Library [![Build Status](https://travis-ci.org/arista-eosplus/pyeapi.svg?branch=develop)](https://travis-ci.org/arista-eosplus/pyeapi) [![Coverage Status](https://coveralls.io/repos/github/arista-eosplus/pyeapi/badge.svg?branch=develop)](https://coveralls.io/github/arista-eosplus/pyeapi?branch=develop) [![Documentation Status](https://readthedocs.org/projects/pyeapi/badge/?version=latest)](http://readthedocs.org/docs/pyeapi/en/latest/?badge=latest) The Python library for Arista's eAPI command API implementation provides a client API work using eAPI and communicating with EOS nodes. The Python library can be used to communicate with EOS either locally (on-box) or remotely (off-box). It uses a standard INI-style configuration file to specify one or more nodes and connection properties. The pyeapi library also provides an API layer for building native Python objects to interact with the destination nodes. The API layer is a convenient implementation for working with the EOS configuration and is extensible for developing custom implementations. This library is freely provided to the open source community for building robust applications using Arista EOS. Support is provided as best effort through Github issues. ## Documentation * [Quickstart] [quickstart] * [Installation] [install] * [Modules] [modules] * [Release Notes] [rns] * [Contribute] [contribute] ### Building Local Documentation If you cannot access readthedocs.org you have the option of building the documentation locally. 1. ``pip install -r dev-requirements.txt`` 2. ``cd docs`` 3. ``make html`` 4. ``open _build/html/index.html`` # License Copyright (c) 2015, Arista Networks EOS+ All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Arista nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [pyeapi]: https://github.com/arista-eosplus/pyeapi [quickstart]: http://pyeapi.readthedocs.org/en/master/quickstart.html [install]: http://pyeapi.readthedocs.org/en/master/install.html [contribute]: http://pyeapi.readthedocs.org/en/master/contribute.html [modules]: http://pyeapi.readthedocs.org/en/master/modules.html [support]: http://pyeapi.readthedocs.org/en/master/support.html [rns]: http://pyeapi.readthedocs.org/en/master/release-notes.html pyeapi-1.0.2/VERSION0000644000076500000240000000000614447406053015212 0ustar dlyssenkostaff000000000000001.0.2 pyeapi-1.0.2/dev-requirements.txt0000644000076500000240000000035514447405743020216 0ustar dlyssenkostaff00000000000000-r requirements.txt mock coveralls twine check-manifest pycodestyle pyflakes coverage sphinx sphinxcontrib-napoleon flake8 #commenting below 2 lines until switchover to python3 is complete #flake8-print #flake8-debugger sphinx_rtd_theme pyeapi-1.0.2/docs/0000755000076500000240000000000014447406213015074 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/docs/Makefile0000644000076500000240000001554614447405743016556 0ustar dlyssenkostaff00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build APIDIR = api_modules CLIENTDIR = client_modules CWD := '$(shell pwd)' # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* rm -rf $(CLIENTDIR)/* rm -rf $(APIDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." modules: python $(CWD)/generate_modules.py docs: clean modules html dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonClientforeAPI.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonClientforeAPI.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonClientforeAPI" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonClientforeAPI" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." pyeapi-1.0.2/docs/api_modules/0000755000076500000240000000000014447406213017375 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/docs/api_modules/_list_of_modules.rst0000644000076500000240000000043114447406160023454 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Api === .. toctree:: :maxdepth: 2 abstract acl bgp interfaces ipinterfaces mlag ntp ospf routemaps staticroute stp switchports system users varp vlans vrfs vrrp pyeapi-1.0.2/docs/api_modules/abstract.rst0000644000076500000240000000031014447406160021725 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Abstract ======== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.abstract :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/acl.rst0000644000076500000240000000027114447406160020667 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Acl === .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.acl :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/bgp.rst0000644000076500000240000000027114447406160020700 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Bgp === .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.bgp :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/interfaces.rst0000644000076500000240000000031614447406160022253 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Interfaces ========== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.interfaces :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/ipinterfaces.rst0000644000076500000240000000032414447406160022603 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Ipinterfaces ============ .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.ipinterfaces :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/mlag.rst0000644000076500000240000000027414447406160021053 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Mlag ==== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.mlag :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/ntp.rst0000644000076500000240000000027114447406160020731 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Ntp === .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.ntp :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/ospf.rst0000644000076500000240000000027414447406160021102 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Ospf ==== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.ospf :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/routemaps.rst0000644000076500000240000000031314447406160022144 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Routemaps ========= .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.routemaps :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/staticroute.rst0000644000076500000240000000032114447406160022472 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Staticroute =========== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.staticroute :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/stp.rst0000644000076500000240000000027114447406160020736 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Stp === .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.stp :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/switchports.rst0000644000076500000240000000032114447406160022515 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Switchports =========== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.switchports :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/system.rst0000644000076500000240000000030214447406160021447 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py System ====== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.system :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/users.rst0000644000076500000240000000027714447406160021277 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Users ===== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.users :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/varp.rst0000644000076500000240000000027414447406160021103 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Varp ==== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.varp :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/vlans.rst0000644000076500000240000000027714447406160021261 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Vlans ===== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.vlans :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/vrfs.rst0000644000076500000240000000027414447406160021113 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Vrfs ==== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.vrfs :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/api_modules/vrrp.rst0000644000076500000240000000027414447406160021124 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Vrrp ==== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.api.vrrp :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/client_modules/0000755000076500000240000000000014447406213020102 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/docs/client_modules/_list_of_modules.rst0000644000076500000240000000021014447406160024154 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Client ====== .. toctree:: :maxdepth: 2 client eapilib utils pyeapi-1.0.2/docs/client_modules/client.rst0000644000076500000240000000027614447406160022120 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Client ====== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.client :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/client_modules/eapilib.rst0000644000076500000240000000030114447406160022234 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Eapilib ======= .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.eapilib :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/client_modules/utils.rst0000644000076500000240000000027314447406160021777 0ustar dlyssenkostaff00000000000000.. This file has been autogenerated by generate_modules.py Utils ===== .. toctree:: :maxdepth: 1 .. automodule:: pyeapi.utils :members: :undoc-members: :show-inheritance: pyeapi-1.0.2/docs/conf.py0000755000076500000240000002503714447405743016414 0ustar dlyssenkostaff00000000000000# -*- coding: utf-8 -*- # # Python Client for eAPI documentation build configuration file, created by # sphinx-quickstart on Sat Mar 28 22:43:11 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import re import sphinx_rtd_theme # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('..') ) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinx.ext.doctest', 'sphinx.ext.napoleon' ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Python Client for eAPI' copyright = u'2015, Arista EOS+ CS' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. release = open('../VERSION').read().split()[0].strip() # Assume PEP 440 version strings p = re.compile(r'(\d+!)?((\d+)(.\d+)*(.\d+)*)(.?[a|b|rc]\d*)?(.post\d*)?(.dev\d*)?', re.IGNORECASE) vers = p.search(release) if vers: version = vers.group(2) else: version = 'develop' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'PythonClientforeAPIdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'PythonClientforeAPI.tex', u'Python Client for eAPI Documentation', u'Arista EOS+ CS', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pythonclientforeapi', u'Python Client for eAPI Documentation', [u'Arista EOS+ CS'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'PythonClientforeAPI', u'Python Client for eAPI Documentation', u'Arista EOS+ CS', 'PythonClientforeAPI', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. epub_title = u'Python Client for eAPI' epub_author = u'Arista EOS+ CS' epub_publisher = u'Arista EOS+ CS' epub_copyright = u'2015, Arista EOS+ CS' # The basename for the epub file. It defaults to the project name. #epub_basename = u'Python Client for eAPI' # The HTML theme for the epub output. Since the default themes are not optimized # for small screen space, using the same theme for HTML and epub output is # usually not wise. This defaults to 'epub', a theme designed to save visual # space. #epub_theme = 'epub' # The language of the text. It defaults to the language option # or en if the language is not set. #epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. #epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. #epub_identifier = '' # A unique identification for the text. #epub_uid = '' # A tuple containing the cover image and cover page html template filenames. #epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. #epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. #epub_post_files = [] # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] # The depth of the table of contents in toc.ncx. #epub_tocdepth = 3 # Allow duplicate toc entries. #epub_tocdup = True # Choose between 'default' and 'includehidden'. #epub_tocscope = 'default' # Fix unsupported image types using the PIL. #epub_fix_images = False # Scale large images. #epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. #epub_show_urls = 'inline' # If false, no index is generated. #epub_use_index = True pyeapi-1.0.2/docs/configfile.rst0000644000076500000240000001677714447405743017764 0ustar dlyssenkostaff00000000000000.. _configfile: ######################### Pyeapi Configuration File ######################### The pyeapi configuration file is a convenient place to store node connection information. By keeping connection information central, your pyeapi scripts can effortlessly reference nodes without any manual import of credentials or location information. Therefore, the pyeapi configuration file becomes a reflection of your switch inventory and the way in which the EOS Command API is enabled on the node. The following explains how to craft your pyeapi configuration file to address specific implementation styles. .. contents:: :depth: 2 ******************** eapi.conf Parameters ******************** The following configuration options are available for defining node entries: :host: The IP address or FQDN of the remote device. If the host parameter is omitted then the connection name is used :username: The eAPI username to use for authentication (only required for http or https connections) :password: The eAPI password to use for authentication (only required for http or https connections) :enablepwd: The enable mode password if required by the destination node :transport: Configures the type of transport connection to use. Valid values are: - socket (default, available in EOS 4.14.5 or later) - http_local (available in EOS 4.14.5 or later) - http - https - https_certs :port: Configures the port to use for the eAPI connection. A default port is used if this parameter is absent, based on the transport setting using the following values: - transport: http, default port: 80 - transport: https, default port: 443 - transport: https_certs, default port: 443 - transport: http_local, default port: 8080 - transport: socket, default port: n/a ********************************* When is an eapi.conf file needed? ********************************* It's important to understand the nuances of the pyeapi configuration file so you can simplify your implementation. Here's a quick summary of when the eapi.conf file is needed: =========== ================== =============== ======================== Transport eapi.conf Required Script run from Authentication Required =========== ================== =============== ======================== http Yes On/Off-switch Yes https Yes On/Off-switch Yes https_certs Yes On/Off-switch Yes (Auth done via certs, not un/pw) http_local Yes On-switch only No socket No On-switch only No =========== ================== =============== ======================== ******************************** Where should the file be placed? ******************************** ============ ================================================= Search Order Search Location ============ ================================================= 1 Environment Variable EAPI_CONF=/path/to/eapi.conf 2 $HOME/.eapi.conf 3 /mnt/flash/eapi.conf ============ ================================================= .. Note:: There is a slight difference in #2 ``.eapi.conf`` versus #3 ``eapi.conf`` ************************************ eapi.conf for On-box Implementations ************************************ This method would be used to run a pyeapi script on-box. In this mode, eAPI can be configured to require or not require authentication depending upon how you enabled EOS Command API. Notice from the table above, that if EOS Command API Unix Sockets are enabled, or HTTP Local, you get the benefit of not needing to pass in credentials since the connection can only be made from localhost and it assumes the user has already authenticated to get on the switch. Using Unix Sockets ================== This is the preferred method. The default transport for pyeapi is ``socket`` and the default host is ``localhost``. Therefore, if running a pyeapi script on-box and have Unix Sockets enabled, you do not need an eapi.conf, nor do you need to pass any credentials (quite handy!). .. Note:: Unix Sockets are supported on EOS 4.14.5+ Using HTTP Local ================ As the table above indicates, a pyeapi configuration file is required in ``/mnt/flash/eapi.conf``. It would contain something like: .. code-block:: console [connection:localhost] transport: http_local Pay attention: once ``eapi.conf`` exists, the respective protocol method must be configured on the box. E.g., for the above ``eapi.conf`` sample, the following configuration must also exist: .. code-block:: console switch(config)#management http-server switch(config-mgmt-http-server)#protocol http localhost Using HTTP or HTTPS =================== As the table above indicates, a pyeapi configuration file is required in ``/mnt/flash/eapi.conf``. It would contain something like: .. code-block:: console [connection:localhost] transport: http[s] username: admin password: admin ************************************* eapi.conf for Off-box Implementations ************************************* This method would be used to run a pyeapi script from another server. In this mode, eAPI will require authentication. The only real option is whether you connect over HTTP or HTTPS. .. Note:: The ``socket`` and ``http_local`` transport options are not applicable. Notice from the table above, that if EOS Command API Unix Sockets are enabled, or HTTP Local, you get the benefit of not needing to pass in credentials since the connection can only be made from localhost and it assumes the user has already authenticated to get on the switch. Using HTTP or HTTPS =================== As the table above indicates, a pyeapi configuration file is required in ``$HOME/.eapi.conf``. It would contain something like: .. code-block:: console [connection:veos01] transport: http username: paul password: nottelling [connection:veos03] transport: https username: bob password: mysecret [connection:veos04] host: 192.168.2.10 transport: https username: admin password: admin .. Note:: avoid using ``localhost`` name in the connection description (i.e.: ``[connection:localhost]``). The name ``localhost`` is reserved solely for ``on-box`` connection method and it won't work when connecting ``off-box`` Using HTTPS with Certificates ============================= The https_certs transport options allows users to do authentication for pyeapi with certificates instead of username/password. This requires functional certificate chains are setup, copied to the proper location and trusted by EOS beforehand. The ca_file parameter is optional. If provided the switches certificate will also be validated against the provided CA cert. If no CA cert is provided then no server side validation will be done. .. code-block:: console [connection:veos01] transport: https_certs cert_file: /path/to/certificate/file key_file: /path/to/private/key/file ca_file: /path/to/CA/certificate/file [connection:veos02] transport: https_certs cert_file: /path/to/certificate/file key_file: /path/to/private/key/file ******************* The DEFAULT Section ******************* The [DEFAULT] section can be used to gather globally applicable settings. Say that all of your nodes use the same transport or username, you can do something like: .. code-block:: console [connection:veos01] [connection:veos03] transport: https username: bob password: mysecret [connection:veos04] host: 192.168.2.10 [DEFAULT] transport: https username: admin password: admin pyeapi-1.0.2/docs/configsessions.rst0000644000076500000240000000322014447405743020666 0ustar dlyssenkostaff00000000000000Using Config Sessions via Python Client for eAPI ======================================================= Config Sessions can be used via Pyeapi. Config sessions allow a block of config to be added as one operation instead of as individual lines. Configurations applied in this manner allow the user to abort all the config being applied if an error occurs. Using Config Sessions: .. code-block:: python import pyeapi node = pyeapi.connect_to('veos01') vlans = node.api('vlans') node.configure_session() node.diff() # Sends "configure session 9c27d0e8-afef-4afd-95ae-3e3200bb7a3e" and "show session-config diff" """ => --- system:/running-config +++ session:/9c27d0e8-afef-4afd-95ae-3e3200bb7a3e-session-config @@ -32,7 +32,7 @@ ! clock timezone Asia/Tokyo ! -vlan 1000,3001-3006 +vlan 100,1000,3001-3006 ! interface Port-Channel1 switchport trunk allowed vlan 3001-3003 """ node.abort() # Sends "configure session 9c27d0e8-afef-4afd-95ae-3e3200bb7a3e" and "abort" # or node.commit() # Sends "configure session 9c27d0e8-afef-4afd-95ae-3e3200bb7a3e" and "commit" Config Session with invalid config line: .. code-block:: python node = pyeapi.connect_to('veos01') interfaces = node.api('interfaces') node.configure_session() if not (interfaces.configure(['interface Eth6', 'no switchport', 'ip address 172.16.0.1/30']) and \ interfaces.configure(['interface Eth7', 'no switchport', 'ip address 172.16.0.1/30'])): node.abort() # This aborts everything!! For more detailed information about using Configure Sessions in EOS, reference the user manual for the version of EOS running on your switch. pyeapi-1.0.2/docs/contribute.rst0000644000076500000240000000204214447405743020011 0ustar dlyssenkostaff00000000000000 ########## Contribute ########## The Arista EOS+ team is happy to accept community contributions to the Pyeapi project. Please note that all contributions that modify the library behavior require corresponding test cases otherwise the pull request will be rejected. ******* Testing ******* The pyeapi library provides both unit tests and system tests. The unit tests can be run without an EOS node. To run the system tests, you will need to update the ``dut.conf`` file found in test/fixtures. Unit Test ========= To run the unit tests, simply run ``make unittest`` from the root of the pyeapi source folder System Test =========== To run the system tests, simply run ``make systest`` from the root of the pyeapi source folder. .. Tip:: To run all tests, use ``make tests`` from the root of the pyeapi source folder ******** Coverage ******** Contributions should maintain 100% code coverage. You can check this locally before submitting your Pull Request. 1. Run ``make unittest`` 2. Run ``make coverage_report`` to confirm code coverage. pyeapi-1.0.2/docs/description.rst0000644000076500000240000000154614447405743020166 0ustar dlyssenkostaff00000000000000The Python Client for eAPI ========================== The Python Client for eAPI (pyeapi) is a native Python library wrapper around Arista EOS eAPI. It provides a set of Python language bindings for configuring Arista EOS nodes. The Python library can be used to communicate with EOS either locally (on-box) or remotely (off-box). It uses a standard INI-style configuration file to specify one or more nodes and connection profiles. The pyeapi library also provides an API layer for building native Python objects to interact with the destination nodes. The API layer is a convenient implementation for working with the EOS configuration and is extensible for developing custom implementations. This library is freely provided to the open source community for building robust applications using Arista EOS. Support is provided as best effort through Github issues. pyeapi-1.0.2/docs/examples.rst0000644000076500000240000000017114447405743017452 0ustar dlyssenkostaff00000000000000Configuration Examples Using pyeapi =================================== .. toctree:: :maxdepth: 1 subinterfaces pyeapi-1.0.2/docs/generate_modules.py0000755000076500000240000000543514447405743021011 0ustar dlyssenkostaff00000000000000#!/usr/bin/python3 from os import listdir, path, makedirs from os.path import isfile, join, exists import pprint as pp HERE = path.abspath(path.dirname(__file__)) MODULES_PATH = '%s/../pyeapi/' % HERE AUTOGEN = '.. This file has been autogenerated by generate_modules.py\n\n' def get_module_names(p): '''Accepts a path to search for modules. The method will filter on files that end in .pyc or files that start with __. Arguments: p (string): The path to search Returns: list of file names ''' mods = list() mods = [f.split('.')[0] for f in listdir(p) if isfile(join(p, f)) and not f.endswith('.pyc') and not f.startswith('__')] print( len(mods) ) return mods def process_modules(modules): '''Accepts dictionary of 'client' and 'api' modules and creates the corresponding files. ''' for mod in modules['client']: directory = '%s/client_modules' % HERE if not exists(directory): makedirs(directory) write_module_file(mod, directory, 'pyeapi') for mod in modules['api']: directory = '%s/api_modules' % HERE if not exists(directory): makedirs(directory) write_module_file(mod, directory, 'pyeapi.api') create_index(modules) def create_index(modules): '''This takes a dict of modules and created the RST index file.''' for key in modules.keys(): file_path = join(HERE, '%s_modules/_list_of_modules.rst' % key) list_file = open(file_path, 'w') # Write the generic header list_file.write('%s\n' % AUTOGEN) list_file.write('%s\n' % key.title()) list_file.write('=' * len(key)) list_file.write('\n\n') list_file.write('.. toctree::\n') list_file.write(' :maxdepth: 2\n\n') for module in modules[key]: list_file.write(' %s\n' % module) def write_module_file(name, path, package): '''Creates an RST file for the module name passed in. It places it in the path defined ''' file_path = join(path, '%s.rst' % name) mod_file = open(file_path, 'w') mod_file.write('%s\n' % AUTOGEN) mod_file.write('%s\n' % name.title()) mod_file.write('=' * len(name)) mod_file.write('\n\n') mod_file.write('.. toctree::\n') mod_file.write(' :maxdepth: 1\n\n') mod_file.write('.. automodule:: %s.%s\n' % (package, name)) mod_file.write(' :members:\n') mod_file.write(' :undoc-members:\n') mod_file.write(' :show-inheritance:\n') def main(): modules = dict(client=None, api=None) modules['client'] = get_module_names(MODULES_PATH) modules['api'] = get_module_names('%s/api' % MODULES_PATH) modules['client'].sort() modules['api'].sort() process_modules(modules) if __name__ == '__main__': main() pyeapi-1.0.2/docs/index.rst0000644000076500000240000000214014447405743016741 0ustar dlyssenkostaff00000000000000.. _intro: ############ Introduction ############ The Python Client for eAPI (pyeapi) is a native Python library wrapper around Arista EOS eAPI. It provides a set of Python language bindings for configuring Arista EOS nodes. The Python library can be used to communicate with EOS either locally (on-box) or remotely (off-box). It uses a standard INI-style configuration file to specify one or more nodes and connection profiles. The pyeapi library also provides an API layer for building native Python objects to interact with the destination nodes. The API layer is a convenient implementation for working with the EOS configuration and is extensible for developing custom solutions. This library is freely provided to the open source community for building robust applications using Arista EOS. Support is provided as best effort through Github issues. .. toctree:: :maxdepth: 1 install quickstart configfile modules requirements examples contribute release-notes support license ****************** Indices and tables ****************** * :ref:`genindex` * :ref:`modindex` pyeapi-1.0.2/docs/install.rst0000644000076500000240000000763414447405743017315 0ustar dlyssenkostaff00000000000000.. _install: ############ Installation ############ The installation of pyeapi is straightforward and simple. As mentioned in the :ref:`intro`, pyeapi can be run on-box or off-box. The instructions below will provide some tips to help you for either platform. .. contents:: :depth: 3 *********************** Pip with Network Access *********************** If your platform has internet access you can use the Python Package manager to install pyeapi .. code-block:: console admin:~ admin$ sudo pip install pyeapi .. Note:: You will likely notice Pip install netaddr, a dependency of pyeapi. Pip - Upgrade Pyeapi ==================== .. code-block:: console admin:~ admin$ sudo pip install --upgrade pyeapi ************************** Pip without Network Access ************************** If you want to install pyeapi on a switch with no internet access: **Step 1:** Download Pypi Package - `Download `_ the latest version of **pyeapi** on your local machine. - You will also need a dependency package `netaddr `_. **Step 2:** SCP both files to the Arista switch and install .. code-block:: console admin:~ admin$ scp path/to/pyeapi-.tar.gz ansible@veos01:/mnt/flash/ admin:~ admin$ scp path/to/netaddr-.tar.gz ansible@veos01:/mnt/flash/ Then SSH into your node and install it. Be sure to replace ```` with the actual filename: .. code-block:: console [admin@veos ~]$ sudo pip install /mnt/flash/netaddr-.tar.gz [admin@veos ~]$ sudo pip install /mnt/flash/pyeapi-.tar.gz These packages must be re-installed on reboot. Therefore, add the install commands to ``/mnt/flash/rc.eos`` to trigger the install on reboot: .. code-block:: console [admin@veos ~]$ vi /mnt/flash/rc.eos Add the lines (the #! may already exist in your rc.eos) .. code-block:: console #!/bin/bash sudo pip install /mnt/flash/netaddr-.tar.gz sudo pip install /mnt/flash/pyeapi-.tar.gz ************************************ Development - Run pyeapi from Source ************************************ .. Tip:: We recommend running pyeapi in a virtual environment. For more information, `read this. `_ These instructions will help you install and run pyeapi from source. This is useful if you plan on contributing or if you'd always like to see the latest code in the develop branch. .. Important:: These steps require Pip and Git **Step 1:** Clone the pyeapi Github repo .. code-block:: shell # Go to a directory where you'd like to keep the source admin:~ admin$ cd ~/projects admin:~ admin$ git clone https://github.com/arista-eosplus/pyeapi.git admin:~ admin$ cd pyeapi **Step 2:** Check out the desired version or branch .. code-block:: shell # Go to a directory where you'd like to keep the source admin:~ admin$ cd ~/projects/pyeapi # To see a list of available versions or branches admin:~ admin$ git tag admin:~ admin$ git branch # Checkout the desired version of code admin:~ admin$ git checkout v0.3.3 **Step 3:** Install pyeapi using Pip with -e switch .. code-block:: shell # Go to a directory where you'd like to keep the source admin:~ admin$ cd ~/projects/pyeapi # Install admin:~ admin$ sudo pip install -e ~/projects/pyeapi **Step 4:** Install pyeapi requirements .. code-block:: shell # Go to a directory where you'd like to keep the source admin:~ admin$ pip install -r dev-requirements.txt .. Tip:: If you start using pyeapi and get import errors, make sure your PYTHONPATH is set to include the path to pyeapi. Development - Upgrade Pyeapi ============================ .. code-block:: shell admin:~ admin$ cd ~/projects/pyeapi admin:~ admin$ git pull .. Note:: If you followed the directions above and used ``pip install -e``, pip will automatically use the updated code. pyeapi-1.0.2/docs/license.rst0000644000076500000240000000274014447405743017262 0ustar dlyssenkostaff00000000000000License ======= Copyright (c) 2015, Arista Networks EOS+ All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Arista nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pyeapi-1.0.2/docs/modules.rst0000644000076500000240000000016214447405743017304 0ustar dlyssenkostaff00000000000000Modules ======= .. toctree:: :maxdepth: 4 client_modules/_list_of_modules api_modules/_list_of_modules pyeapi-1.0.2/docs/quickstart.rst0000644000076500000240000000757414447405743020044 0ustar dlyssenkostaff00000000000000########## Quickstart ########## In order to use pyeapi, the EOS command API must be enabled using configuration mode. This library supports eAPI calls over both HTTP/S and UNIX Domain Sockets. Once the command API is enabled on the destination node, create a configuration file with the node properties. There are some nuances about the configuration file that are important to understand; take a minute and read about the :ref:`configfile`. ********************** Enable EOS Command API ********************** Refer to your official Arista EOS Configuration Guide to learn how to enable EOS Command API. Depending upon your software version, the options available include: - HTTP - HTTPS - HTTPS Certificates - HTTP Local - Unix Socket ************** Install Pyeapi ************** Follow the instructions on the :ref:`install` guide to prepare your node for pyeapi. ************************ Create an eapi.conf file ************************ Follow the instructions on the :ref:`configfile` guide to create a pyeapi configuration file. You can skip this step if you are running the pyeapi script on-box and Unix Sockets are enabled for EOS Command API. ***************** Connect to a Node ***************** The Python client for eAPI was designed to be easy to use and implement for writing tools and applications that interface with the Arista EOS management plane. Once EOS is configured properly and the config file created, getting started with a connection to EOS is simple. Below demonstrates a basic connection using pyeapi. For more examples, please see the `examples `_ folder on Github. This first example shows how to instantiate the Node object. The Node object provides some helpful methods and attributes to work with the switch. .. code-block:: python # start by importing the library import pyeapi # create a node object by specifying the node to work with node = pyeapi.connect_to('veos01') # send one or more commands to the node node.enable('show hostname') [{'command': 'show hostname', 'encoding': 'json', 'result': {u'hostname': u'veos01', u'fqdn': u'veos01.arista.com'}}] # Request a specific revision of a command that has been updated node.enable({'cmd': 'show cvx', 'revision': 2}) [{'command': {'cmd': 'show cvx', 'revision': 2}, 'encoding': 'json', 'result': {u'clusterMode': False, u'controllerUUID': u'', u'enabled': False, u'heartbeatInterval': 20.0, u'heartbeatTimeout': 60.0}}] # use the config method to send configuration commands node.config('hostname veos01') [{}] # multiple commands can be sent by using a list # (works for both enable or config) node.config(['interface Ethernet1', 'description foo']) [{}, {}] # return the running or startup configuration from the # node (output omitted for brevity) node.running_config node.startup_config The pyeapi library provides both a client for send and receiving commands over eAPI as well as an API for working directly with EOS resources. The API is designed to be easy and straightforward to use yet also extensible. Below is an example of working with the ``vlans`` API .. code-block:: python # create a connection to the node import pyeapi node = pyeapi.connect_to('veos01') # get the instance of the API (in this case vlans) vlans = node.api('vlans') # return all vlans from the node vlans.getall() {'1': {'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []}, '10': {'state': 'active', 'name': 'VLAN0010', 'vlan_id': 10, 'trunk_groups': []}} # return a specific vlan from the node vlans.get(1) {'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []} # add a new vlan to the node vlans.create(100) True # set the new vlan name vlans.set_name(100, 'foo') True pyeapi-1.0.2/docs/release-notes-0.1.0.rst0000644000076500000240000000035214447405743021035 0ustar dlyssenkostaff00000000000000###### v0.1.0 ###### 2015-01-23 - initial public release of pyeapi - initial support for vlans - initial support for interfaces - initial support for spanningtree - initial support for switchports - initial support for ipinterfaces pyeapi-1.0.2/docs/release-notes-0.1.1.rst0000644000076500000240000000071314447405743021037 0ustar dlyssenkostaff00000000000000###### v0.1.1 ###### 2015-02-17 - adds introspection properties to CommandError for more details (#4) - changed the default transport from HTTP to HTTPS to align with EOS - updates the message returned if the connection profile name is not found - fixes connection name not copied to host parameter if host not configured - fixes an issue where an ipinterface wasnt properly recognized - fixes an issue where a switchport interface was propertly recognized pyeapi-1.0.2/docs/release-notes-0.2.0.rst0000644000076500000240000000073214447405743021040 0ustar dlyssenkostaff00000000000000###### v0.2.0 ###### 2015-03-19 - adds udp_port, vlans and flood_list attributes to vxlan interfaces - renames spanningtree api module to stp for consistency - depreciated spanningtree api module in favor of stp - interfaces module now properly responds to hasattr calls - fixes an issue with collecting the vxlan global flood list from the config - fixes an issue with properly parsing portchannel configurations - adds portfast_type attribute to stp interfaces resource pyeapi-1.0.2/docs/release-notes-0.2.1.rst0000644000076500000240000000014114447405743021033 0ustar dlyssenkostaff00000000000000###### v0.2.1 ###### 2015-03-28 - restores default certificate validation behavior for py2.7.9 pyeapi-1.0.2/docs/release-notes-0.2.2.rst0000644000076500000240000000015214447405743021036 0ustar dlyssenkostaff00000000000000###### v0.2.2 ###### 2015-04-15 - fixes an issue with eAPI error messages that do not return a data key pyeapi-1.0.2/docs/release-notes-0.2.3.rst0000644000076500000240000000013014447405743021033 0ustar dlyssenkostaff00000000000000###### v0.2.3 ###### 2015-04-29 - fixes issue with importing syslog module on Windows pyeapi-1.0.2/docs/release-notes-0.2.4.rst0000644000076500000240000000012414447405743021037 0ustar dlyssenkostaff00000000000000###### v0.2.4 ###### 2015-04-30 - adds required docs/description.rst for setup.py pyeapi-1.0.2/docs/release-notes-0.3.0.rst0000644000076500000240000000046014447405743021037 0ustar dlyssenkostaff00000000000000###### v0.3.0 ###### 2015-05-04 - fixes an issue with configuring stp portfast edge correctly - fixes #13 - fixes #11 - added initial support for system api module - added initial support for acl api module (standard) - added initial api support for mlag configuration - added tag feature to eapi.conf pyeapi-1.0.2/docs/release-notes-0.3.1.rst0000644000076500000240000000034414447405743021041 0ustar dlyssenkostaff00000000000000###### v0.3.1 ###### 2015-06-14 - make pyeapi compatible under Python 3.4 with all unit tests passing ok - added socket_error property to connection to capture socket errors - adds function to create per vlan vtep flood lists pyeapi-1.0.2/docs/release-notes-0.3.2.rst0000644000076500000240000000015114447405743021036 0ustar dlyssenkostaff00000000000000###### v0.3.2 ###### 2015-07-16 - fixes a problem with parsing the hostname value in the system module pyeapi-1.0.2/docs/release-notes-0.3.3.rst0000644000076500000240000000023314447405743021040 0ustar dlyssenkostaff00000000000000###### v0.3.3 ###### 2015-07-31 - added initial support for bgp api module - add trunk group functionality to switchports - add ip routing to system api pyeapi-1.0.2/docs/release-notes-0.4.0.rst0000644000076500000240000000773414447405743021053 0ustar dlyssenkostaff00000000000000###### v0.4.0 ###### 2015-11-05 New APIs ^^^^^^^^ * Add VRRP (`57 `_) [`grybak `_] Add support for VRRP configuration. * Add Staticroute (`45 `_) [`grybak `_] The staticroute API enables you to set static IPv4 routes on your EOS device. * Add VARP (`43 `_) [`phil-arista `_] The Varp API includes the subclass VarpInterfaces. These two combine to provide methods to set virtual IP addresses on interfaces as well as set the global virtual-router mac-address. * Add Routemap (`40 `_) [`phil-arista `_] .. comment Enhancements ^^^^^^^^^^^^ * Making configure RADIUS compatible (`53 `_) [`GaryCarneiro `_] Modifies the syntax of the ``config`` method to use ``configure terminal`` instead of just ``configure``. * Close #46 (`47 `_) [`phil-arista `_] This enhancement allows you to set the LACP Mode while executing the set_members method. The call would look like ``node.api('interfaces').set_members(1, [Ethernet1,Ethernet2], mode='active')`` * Added support to specify timeout (`41 `_) [`dbarrosop `_] This enhancement provides a way to specify a connection timeout. The default is set to 60 seconds. * Add BGP maximum-paths support (`36 `_) [`phil-arista `_] This enhancement adds more attributes to ``eos_bgp_config``. This provides the ability to configure ``maximum-paths N ecmp M`` in your ``router bgp R`` configuration. * Add sshkey support to users API (`34 `_) [`phil-arista `_] This enhancement augments the ``users`` API to now support SSH Keys. Fixed ^^^^^ * client.py 'def enable' returned dictionary key inconsistency (`35 `_) The key that's supposed to be returned is ``result`` but instead the method formerly returned the key ``response``. For now, both ``response`` and ``result`` will be returned with the same data, but ``response`` will be removed in a future release. * [API Users] Can't run set_role with no value (`33 `_) The node.api('users').set_role('test') method didn't remove the role or default the role as you would expect. This bug fix resolves that. * [API Users] Can't run set_privilege with no value (`32 `_) The set_privilege('user') method did not properly negate the privilege level when no argument was passed into the method. * [ API interfaces ] get_members regex wrongly includes PeerEthernet when lag is up (`28 `_) The get_members() method wrongly included a duplicate member when the ``show port-channel N all-ports`` showed the PeerEthernetX. The regular expression has been updated to ignore these entries. * [API] users - can't create password with non-alpha/int characters (`23 `_) The characters ``(){}[]`` cannot be part of a username. Documentation has been updated to reflect this. * Users with sha512 passwords don't get processed correctly using api('users').getall() (`22 `_) Fixed regex to extract the encrypted passwords accurately. Known Caveats ^^^^^^^^^^^^^ * failure when eapi.conf is not formatted correctly (`38 `_) .. comment pyeapi-1.0.2/docs/release-notes-0.5.0.rst0000644000076500000240000000522014447405743021040 0ustar dlyssenkostaff00000000000000###### v0.5.0 ###### 2016-02-16 New APIs ^^^^^^^^ * NTP module (`72 `_) [`grybak-arista `_] Add NTP functionality. Enhancements ^^^^^^^^^^^^ * Add banner support to System API (`75 `_) [`dathelen `_] Add API support for EOS banners and motd. * Issue #18 performance fixes (`74 `_) [`cheynearista `_] Rework underlying HTTP transport to improve receive performance. * Redmine issue 648 (`73 `_) [`grybak-arista `_] Fix some instances where an empty string as negation would not properly negate the option/command. * setup.py print statement for python 3 (`71 `_) [`mzbenami `_] Reformat print statement to work properly with Python3+ * Implement add ACL with seq nos (`70 `_) [`dathelen `_] Add a sequence number when adding a new ACL entry. * Fix for redmine issues 234 and 268 (`68 `_) [`grybak-arista `_] Reworked some system tests for robustness get_block accepts a config string as well as the default 'running_config' * fix #7 and fix #37 (`67 `_) [`grybak-arista `_] Certain command errors will return more detailed information. The connect() method can optionally return a node object. * Add disable key to existing modules for negation of properties (`65 `_) [`grybak-arista `_] Modules now take disable= to negate the command, rather than overloading value. * Compatibility fix for current mock versions (`64 `_) [`wtucker `_] * Add key error checking to set_tracks (`63 `_) [`grybak-arista `_] .. comment Fixed ^^^^^ * Failure when eapi.conf is not formatted correctly (`38 `_) Adds more robust error handling when parsing eapi.conf files. Also, if an error is found it will syslog the error and continue parsing any other eapi.conf files. Known Caveats ^^^^^^^^^^^^^ pyeapi-1.0.2/docs/release-notes-0.6.0.rst0000644000076500000240000000351314447405743021044 0ustar dlyssenkostaff00000000000000###### v0.6.0 ###### 2016-02-22 New Modules ^^^^^^^^^^^ * None Enhancements ^^^^^^^^^^^^ * Added support for multiline commands without having to pass them as a dictionary (`78 `_) [`dbarrosop `_] (See example below) .. code-block:: python >>> import pyeapi >>> connection = pyeapi.client.connect( ... transport='https', ... host='192.168.56.201', ... username='vagrant', ... password='vagrant', ... port=443, ... timeout=60 ... ) >>> device = pyeapi.client.Node(connection) >>> device.run_commands('show hostname') [{u'hostname': u'localhost', u'fqdn': u'localhost'}] >>> device.run_commands('show banner login') [{u'loginBanner': u''}] >>> my_commands = [ ... 'configure session whatever', ... 'hostname new-hostname', ... 'banner login MULTILINE:This is a new banner\nwith different lines!!!', ... 'end' ... ] >>> >>> device.run_commands(my_commands) [{}, {}, {}, {}] >>> print device.run_commands(['show session-config named whatever diffs'], encoding='text')[0]['output'] --- system:/running-config +++ session:/whatever-session-config @@ -3,6 +3,8 @@ ! boot system flash:/vEOS-lab.swi ! transceiver qsfp default-mode 4x10G +! +hostname new-hostname ! spanning-tree mode mstp ! @@ -22,6 +24,11 @@ ! no ip routing ! +banner login +This is a new banner +with different lines!!! +EOF +! management api http-commands no shutdown ! >>> device.run_commands(['configure session whatever', 'commit']) [{}, {}] >>> device.run_commands('show hostname') [{u'hostname': u'new-hostname', u'fqdn': u'new-hostname'}] >>> device.run_commands('show banner login') [{u'loginBanner': u'This is a new banner\nwith different lines!!!\n'}] >>> pyeapi-1.0.2/docs/release-notes-0.6.1.rst0000644000076500000240000000061014447405743021040 0ustar dlyssenkostaff00000000000000###### v0.6.1 ###### 2016-03-04 (This is a hotfix release) New Modules ^^^^^^^^^^^ * None Enhancements ^^^^^^^^^^^^ * None Fixed ^^^^^ * SR56181 - pyeapi 0.5.0 and 0.6.0 aren't able to execute commands with enable password (`88 `_) A regression was introduced in 0.5.0 which broke passing the enable password to the Node() object. pyeapi-1.0.2/docs/release-notes-0.7.0.rst0000644000076500000240000000236314447405743021047 0ustar dlyssenkostaff00000000000000###### v0.7.0 ###### 2016-09-08 New Modules ^^^^^^^^^^^ * Add OSPF API (`95 `_) [`brigoldberg `_] Big thanks for the community support! Enhancements ^^^^^^^^^^^^ * Enhance Node enable() method (`100 `_) [`dathelen `_] This enhancement adds a send_enable flag to the enable and run_commands Node methods. By default the enable command will be sent, however you can now run commands without prepending the enable. * Finish OSPF API (`99 `_) [`dathelen `_] Create system tests and add unit tests to increase code coverage. * Add Cross-Platform Support for pyeapi (`94 `_) [`grybak-arista `_] Use logging instead of syslog for better cross-platform compatibility. This enhancement provides support for Windows users. Fixed ^^^^^ * Allow dot and hyphen in mlag domain-id (`91 `_) Include handling any character in domain-id string, including dot, hyphen, and space. pyeapi-1.0.2/docs/release-notes-0.8.0.rst0000644000076500000240000000716414447405743021054 0ustar dlyssenkostaff00000000000000###### v0.8.0 ###### 2017-03-14 New Modules ^^^^^^^^^^^ * Base API for VRF support. (`133 `_) [`mharista `_] Added new API module for creating VRFs. In addition to creating, configuring and removing VRFs the updates allow for applying a VRF to an interface and creating a VRF specific OSPF instance. Enhancements ^^^^^^^^^^^^ * Add base extended ACL support. (`135 `_) [`mharista `_] Updated ACL api to include extended ACLs in addition to standard. To create an extended ACL provide the type as extended when creating the ACL (default is standard). Currently extended ACL statements can be added with action, protocol, and source/destination address. The API will determine the type of ACL by name after it has been created for future updates. * Add support for creating and deleting ethernet subinterfaces (`132 `_) [`mharista `_] Allow for creation and deletion of ethernet subinterfaces as part of the EthernetInterface class. Subinterfaces are also supported on PortChannel interfaces. An example using the API to create an ethernet subinterface is provided in the docs. * Add node attributes from show version command (`131 `_) [`mharista `_] Added information from show version as attributes to a node. Version, version number and model are added. Version number is simply the numeric portion of the version. For example 4.17.1 if the version is 4.17.1M. All three parameters are populated from the output of show version when any one of the parameters is accessed the first time. * Add support for eAPI expandAliases parameter (`127 `_) [`mharista `_] Allowed users to provide the expandAliases parameter to eAPI calls. This allows users to use aliased commands via the API. For example if an alias is configured as 'sv' for 'show version' then an API call with sv and the expandAliases parameter will return the output of show version. * Add support for autoComplete parameter. Issue 119 (`123 `_) [`mharista `_] Allows users to use short hand commands in eAPI calls. With this parameter included a user can send 'sh ver' via eAPI to get the output of show version. * expose portchannel attributes :lacp fallback, lacp fallback timeout (`118 `_) [`lruslan `_] Helps for configuring LACP fallback in EOS. Fixed ^^^^^ * API path is hardcoded in EapiConnection.send() (`129 `_) Updated the previously hardcoded path to /command-api in the EAPI connection to use the transport objects path. * Cannot run 'no ip virtual-router mac-address' in eos 4.17.x (`122 `_) Fixed command format for negating an ip virtual-router mac-address. Default and disable forms of the command changed and require the mac-address value in EOS 4.17. Update fixes this for newer versions and is backwards compatible. * Non-standard HTTP/s port would cause connection to fail (`120 `_) Bug fixed in PR (`121 `_) where a port passed in via eapi.conf as a unicode value caused the http connection to fail. pyeapi-1.0.2/docs/release-notes-0.8.1.rst0000644000076500000240000000233514447405743021050 0ustar dlyssenkostaff00000000000000###### v0.8.1 ###### 2017-07-20 New Modules ^^^^^^^^^^^ Enhancements ^^^^^^^^^^^^ Fixed ^^^^^ * hard coded /command-api was probably a safe default (`141 `_) This issue/PR (`142 `_) reverts a change that introduced a bug breaking pyeapi's unix-socket connection. Regardless of using TCP/IP or unix-socket for communicating with the EAPI process the data being sent to the process is formatted in HTTP requests. The HTTP requests should always POST to the /command-api endpoint. The change being reverted by this prevents the unix-socket path from ever erroneously being used as the HTTP endpoint. * Execute does not work well with long running commands (`138 `_) Added socket specific error messages in PR (`144 `_) to help distinguish errors caused by command timeouts. * eAPI does not handle commands sent as unicode strings (`137 `_) Bug fixed in PR (`139 `_) unicode commands converted to strings when detected. pyeapi-1.0.2/docs/release-notes-0.8.2.rst0000644000076500000240000000237314447405743021053 0ustar dlyssenkostaff00000000000000Release 0.8.2 ------------- 2018-02-09 New Modules ^^^^^^^^^^^ Enhancements ^^^^^^^^^^^^ * Support eapi command revision syntax (`158 `_) [`jerearista `_] Support requests for specific revisions of EOS command output .. code-block:: python >>> node.enable({'cmd': 'show cvx', 'revision': 2}) [{'command': {'cmd': 'show cvx', 'revision': 2}, 'encoding': 'json', 'result': {u'clusterMode': False, u'controllerUUID': u'', u'enabled': False, u'heartbeatInterval': 20.0, u'heartbeatTimeout': 60.0}}] * Add clearer error message for bad user credentials. (`152 `_) [`mharista `_] .. comment * Reformat EapiConnection send methods exception handling. (`148 `_) [`mharista `_] .. comment Fixed ^^^^^ * Fix route map getall function to find route maps with a hyphen in the name. (`154 `_) [`mharista `_] .. comment Known Caveats ^^^^^^^^^^^^^ pyeapi-1.0.2/docs/release-notes-0.8.3.rst0000644000076500000240000000055014447405743021047 0ustar dlyssenkostaff00000000000000Release 0.8.3 ------------- 2020-01-26 New Modules ^^^^^^^^^^^ Enhancements ^^^^^^^^^^^^ * Support eapi command revision syntax (`181 `_) * Add ability to use pyeapi with certificates * Added check for 'match' statement to be valid before parsing regex ACL Fixed ^^^^^ Known Caveats ^^^^^^^^^^^^^ pyeapi-1.0.2/docs/release-notes-0.8.4.rst0000644000076500000240000000033514447405743021051 0ustar dlyssenkostaff00000000000000Release 0.8.4 ------------- 2020-11-13 New Modules ^^^^^^^^^^^ Enhancements ^^^^^^^^^^^^ * Add streaming capability for eapi interface * Add Power ppc64le support with CI Fixed ^^^^^ Known Caveats ^^^^^^^^^^^^^ pyeapi-1.0.2/docs/release-notes-1.0.0.rst0000644000076500000240000001316314447405743021041 0ustar dlyssenkostaff00000000000000Release 1.0.0 ------------- 2023-06-06 - This is a Python3 (3.7 onwards) release only (Python2 is no longer supported) - Arista EOS 4.22 or later required (for on-box use cases only) New Modules ^^^^^^^^^^^ Enhancements ^^^^^^^^^^^^ Fixed ^^^^^ * Let ``config`` method to raise ``ConnectionError`` exception (`198 `_) The fix ensures that the behavior is consistent with other methods. * Fixed parsing of VLAN groupings by ``vlans.getall()`` (`200 `_) The fix allows handling a case when multiple VLANs in the configs may be grouped under a common (group) name. * Enhanced ``vlans.get()`` to return an actual list of VLANs (`202 `_) The method used to return the argument itself (e.g., an RE: ``200-.*``), now the actual list of matched VLANs is returned * Fixed a corner crash in ``portsVlans.getall()`` (`213 `_) The crash may occur when the switchport is configured with a VLAN profile * Improved ``switchports.getall()`` behavior (`216 `_) The method will not consider subinterfaces anymore * Improved JSON parsing speed (`166 `_) User may improve the speed by using ``ujson`` or ``rapidjson`` modules. The standard ``json`` is used as a fallback. * Allow user to specify an SSL context (`234 `_) Provided the argument option ``context`` to specify an SSL context in ``connection()`` method. * Fixed user password vulnerability in tracebacks (`238 `_) A user password is exposed in a traceback, which could occur due to invalid credentials. * Added support for login password exclusively for ssh-key authentication (`227 `_) Catching up with EOS CLI which already supports such login password. * Fixed user password vulnerability in debugs (`242 `_) A user password was exposed in user enabled debugs. With this commit the user password is masked out now. * Added option not to include config defaults (`221 `_) Reading running-config or startup-config with default values is not always a desirable behavior. This commit adds an option to control such behavior. * Fixed a corner crash in ``ipinterfaces`` module (`210 `_) Fixed a crash when MTU value is not present in the interface configuration (this is rather a corner case condition). * Fixed a bug where vxlan floodlist might return a bogus data (`193 `_) Fixed the issue with ``interfaces`` module, where ``get()`` method returned vxlan data structure with ``flood_list`` parsed incorrectly. * Improved performance of config parsing (`220 `_) Drastically improved perfromance of config parsing by cacheing section splitting results. This fix also tightens the prior relaxed section match behavior by allowing matching only section line (as vs entire section's content) * Enhanced PR (`220 `_) allowing to match sub-sections (`245 `_) After PR #220, matching subsections was impossible (only entire section could have been matched). This enhancement brings back such functionality. * Added support for a session based authentication (`203 `_) Added a couple new transport options (``http_session`` and ``https_session``) to ``connect()`` method to support a session based authentication. * Added support infrastructure to handle deprecated EOS CLI (`247 `_) The added ``CliVariants`` class helps managing transitions from deprecated EOS CLI (by allowing specifying both, new and deprecated CLI variants). * Added support for parsing secondary ip addresses (`251 `_) The added fix extends the return result for ``get()`` method of ``ipinterfaces`` module with the parsed secondary ip addresses (if present). * A minor fix to include internal exception handling (`252 `_) With this fix the ``pyeapi.eapilib.CommandError`` exception will elaborate on the causes of internal errors. * Enhance parsing of BGP router section with ``asdot`` notation (`256 `_) Allow matching and parsing ``router bgp`` sections if spelled with ``asdot`` notation. * removed and updated deprecations (`204 `_, `212 `_, `235 `_, `260 `_, `262 `_, `263 `_) * documentation fixes and updates (`209 `_, `225 `_, `239 `_, `257 `_, `259 `_) Known Caveats ^^^^^^^^^^^^^ pyeapi-1.0.2/docs/release-notes-1.0.2.rst0000644000076500000240000000110114447406053021023 0ustar dlyssenkostaff00000000000000Release 1.0.2 ------------- 2023-06-30 New Modules ^^^^^^^^^^^ Enhancements ^^^^^^^^^^^^ Fixed ^^^^^ * Fixed a regression introduced by PR#220 (`220 `_) Performance enchancement achieved with cacheing in PR#220 has a side effect: if configuration was read before the config modifications made, then the modifications won't be reflected in the consecutive configuration reads (due to the cached read) * Fixed all failing system tests, plus made some improvements in unit tests. Known Caveats ^^^^^^^^^^^^^ pyeapi-1.0.2/docs/release-notes.rst0000644000076500000240000000133714447406053020402 0ustar dlyssenkostaff00000000000000############# Release Notes ############# .. toctree:: :maxdepth: 2 :titlesonly: release-notes-1.0.2.rst release-notes-1.0.0.rst release-notes-0.8.4.rst release-notes-0.8.3.rst release-notes-0.8.2.rst release-notes-0.8.1.rst release-notes-0.8.0.rst release-notes-0.7.0.rst release-notes-0.6.1.rst release-notes-0.6.0.rst release-notes-0.5.0.rst release-notes-0.4.0.rst release-notes-0.3.3.rst release-notes-0.3.2.rst release-notes-0.3.1.rst release-notes-0.3.0.rst release-notes-0.2.4.rst release-notes-0.2.3.rst release-notes-0.2.2.rst release-notes-0.2.1.rst release-notes-0.2.0.rst release-notes-0.1.1.rst release-notes-0.1.0.rst pyeapi-1.0.2/docs/requirements.rst0000644000076500000240000000047314447405743020364 0ustar dlyssenkostaff00000000000000############ Requirements ############ * Arista EOS 4.22 or later * Arista eAPI enabled for at least one transport (see Official EOS Config Guide at arista.com for details) * Python 3.7+ * Pyeapi requires the netaddr Python module .. Note:: netaddr gets installed automatically if you use pip to install pyeapi pyeapi-1.0.2/docs/subinterfaces.rst0000644000076500000240000000243114447405743020472 0ustar dlyssenkostaff00000000000000Configuring Subinterfaces Using Python Client for eAPI ======================================================= Subinterfaces can be configured on Ethernet and Port-Channel interfaces. To do this in pyeapi simply call create or delete with your subinterface name. Subinterfaces require that the primary interface be in routed mode. .. code-block:: python import pyeapi node = pyeapi.connect_to('veos01') node.api('ipinterfaces').create('Ethernet1') At this point the below should be in your running configuration. .. code-block:: shell ! interface Ethernet1 no switchport ! Next step is to create the subinterface .. code-block:: python node.api('interfaces').create('Ethernet1.1') .. code-block:: shell ! interface Ethernet1 no switchport ! interface Ethernet1.1 ! Subinterfaces also require a vlan to be applied to them. .. code-block:: python node.api('interfaces').set_encapsulation('Ethernet1.1', 4) .. code-block:: shell ! interface Ethernet1 no switchport ! interface Ethernet1.1 encapsulation dot1q vlan 4 ! Using delete in the same format as create will remove the subinterface. For more detailed information about configuring subinterfaces in EOS, reference the user manual for the version of EOS running on your switch. pyeapi-1.0.2/docs/support.rst0000644000076500000240000000223014447405743017346 0ustar dlyssenkostaff00000000000000####### Support ####### ******* Contact ******* Pyeapi is developed by Arista EOS+ CS and supported by the Arista EOS+ community. Support for the code is provided on a best effort basis by the Arista EOS+ CS team and the community. You can contact the team that develops these modules by sending an email to eosplus-dev@arista.com. For customers that are looking for a premium level of support, please contact your local account team or email eosplus@arista.com for help. ***************** Submitting Issues ***************** The Arista EOS+ CS development team uses Github Issues to track discovered bugs and enhancement requests. The issues tracker can be found at https://github.com/arista-eosplus/pyeapi/issues. For defect issues, please provide as much relevant data as possible as to what is causing the issue, if and how it is reproducible, the version of EOS, python, and any OS details. For enhancement requests, please provide a brief description of the enhancement request and the version of EOS to be supported. The issue tracker is monitored by Arista EOS+ CS and issues submitted are categorized and scheduled for inclusion in upcoming Pyeapi versions. pyeapi-1.0.2/examples/0000755000076500000240000000000014447406213015762 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/examples/get_config.py0000644000076500000240000000073714447405743020456 0ustar dlyssenkostaff00000000000000#!/usr/bin/env python from __future__ import print_function import pyeapi pyeapi.load_config('nodes.conf') node = pyeapi.connect_to('veos01') print('Show running-config for veos01') print(('-'*30)) print((node.get_config('running-config'))) print() print() print('Show startup-config for veos01') print(('-'*30)) print((node.get_config('startup-config'))) print() print() print('Show config diffs') print(('-'*30)) print((node.get_config('running-config', 'diffs'))) print() pyeapi-1.0.2/examples/nodes.conf0000644000076500000240000000022514447405743017747 0ustar dlyssenkostaff00000000000000[connection:veos01] host: 192.168.1.16 #username: eapi password: password [connection:veos02] host: 192.168.1.17 username: eapi password: password pyeapi-1.0.2/examples/simple.py0000644000076500000240000000040414447405743017632 0ustar dlyssenkostaff00000000000000#!/usr/bin/env python from __future__ import print_function import pyeapi connection = pyeapi.connect(host='192.168.1.16') output = connection.execute(['enable', 'show version']) print(('My system MAC address is', output['result'][1]['systemMacAddress'])) pyeapi-1.0.2/examples/sysmac.py0000644000076500000240000000040314447405743017637 0ustar dlyssenkostaff00000000000000#!/usr/bin/env python from __future__ import print_function import pyeapi pyeapi.load_config('nodes.conf') node = pyeapi.connect_to('veos01') output = node.enable('show version') print(('My System MAC address is', output[0]['result']['systemMacAddress'])) pyeapi-1.0.2/examples/vlans_api.py0000644000076500000240000000141214447405743020315 0ustar dlyssenkostaff00000000000000#!/usr/bin/env python from __future__ import print_function # import the client library import pyeapi # load the conf file and connect to the node pyeapi.load_config('nodes.conf') node = pyeapi.connect_to('veos01') # get the vlan api and enable autorefresh vlans = node.api('vlans') vlans.autorefresh = True if vlans.get(123): print('Vlan 123 already exists, deleting it') vlans.delete(1234) print('\nCreating Vlan 123') vlans.create(123) print('Setting Vlan 123 name to "test_vlan"') vlans.set_name(123, 'test_vlan') print('\nDisplaying all vlans on the node') print(('-'*30)) for vlan in list(vlans.values()): print((" Vlan Id: {vlan_id}, Name: {name}".format(**vlan))) if vlans.get(123): print('\nDeleting Vlan 123') vlans.delete(123) print() pyeapi-1.0.2/pyeapi/0000755000076500000240000000000014447406213015433 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/pyeapi/__init__.py0000644000076500000240000000326014447406053017547 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # __version__ = '1.0.2' __author__ = 'Arista EOS+' from .client import load_config, connect, connect_to, config_for __all__ = ['load_config', 'connect', 'connect_to', 'config_for'] pyeapi-1.0.2/pyeapi/api/0000755000076500000240000000000014447406213016204 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/pyeapi/api/__init__.py0000644000076500000240000000312314447405743020323 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # from .abstract import Entity, EntityCollection __all__ = ['Entity', 'EntityCollection'] pyeapi-1.0.2/pyeapi/api/abstract.py0000644000076500000240000001751514447405743020401 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Provides an abstract implementation for building API modules This module provides a set of classes that are used to build API modules that work with Node objects. Using this module will allow the API modules to be automatically loaded using the Node.api method. The classes in this module should not be instantiated directly but rather provide parent class for API implementations. All API modules will ultimately derive from BaseEntity which provides some common functions to make building API modules easier. """ from collections.abc import Callable, Mapping from pyeapi.eapilib import CommandError from pyeapi.utils import make_iterable class BaseEntity(object): """Base class for all resources to derive from This BaseEntity class should not be directly instatiated. It is designed to be implemented by all resource classes to provide common methods. Attributes: node (Node): The node instance this resource will perform operations against for configuration config (Config): Returns an instance of Config with the nodes current running configuration error (CommandError): Holds the latest CommandError exception instance if raised Args: node (Node): An instance of Node """ def __init__(self, node): self.node = node @property def version_number(self): return self.node.version_number @property def config(self): return self.node.running_config @property def error(self): return self.node.connection.error def get_block(self, parent, config='running_config'): """ Scans the config and returns a block of code Args: parent (str): The parent string to search the config for and return the block config (str): A text config string to be searched. Default is to search the running-config of the Node. Returns: A string object that represents the block from the config. If the parent string is not found, then this method will return None. """ try: parent = r'^%s$' % parent return self.node.section(parent, config=config) except TypeError: return None def configure(self, commands): """Sends the commands list to the node in config mode This method performs configuration the node using the array of commands specified. This method wraps the configuration commands in a try/except block and stores any exceptions in the error property. Note: If the return from this method is False, use the error property to investigate the exception Args: commands (list): A list of commands to be sent to the node in config mode Returns: True if the commands are executed without exception otherwise False is returned """ try: self.node.config(commands) return True except (CommandError): return False def command_builder(self, string, value=None, default=None, disable=None): """Builds a command with keywords Notes: Negating a command string by overriding 'value' with None or an assigned value that evalutates to false has been deprecated. Please use 'disable' to negate a command. Parameters are evaluated in the order 'default', 'disable', 'value' Args: string (str): The command string value (str): The configuration setting to subsititue into the command string. If value is a boolean and True, just the command string is used default (bool): Specifies the command should use the default keyword argument. Default preempts disable and value. disable (bool): Specifies the command should use the no keyword argument. Disable preempts value. Returns: A command string that can be used to configure the node """ if default: return 'default %s' % string elif disable: return 'no %s' % string elif value is True: return string elif value: return '%s %s' % (string, value) else: return 'no %s' % string # -- above line to be deprecated and replaced with the error below # raise ValueError("abstract.command_builder: No value " # "received '%s'" % value) def configure_interface(self, name, commands): """Configures the specified interface with the commands Args: name (str): The interface name to configure commands: The commands to configure in the interface Returns: True if the commands completed successfully """ commands = make_iterable(commands) commands.insert(0, 'interface %s' % name) return self.configure(commands) class Entity(BaseEntity, Callable): """Abstract class for building Entity resources The Entity class provides an abstract implementation that allows for building an API configuration resource. The Entity class should not be directly instantiated. It is used in instances where a single config entity is appropriate in the configuration. Examples of Entity candidates include global spanning tree """ def __call__(self): return self.get() def get(self): raise NotImplementedError class EntityCollection(BaseEntity, Mapping): """Abstract class for building EntityCollection resources The EntityCollection class provides an abstract implementat that allows for building API configuration resources with multiple resources. The EntityCollection class should not be directly instantiated. Examples of an EntityCollection candidate include VLANs and interfaces """ def __call__(self): return self.getall() def __getitem__(self, value): return self.get(value) def __len__(self): return len(self.getall()) def __iter__(self): return iter(self.getall()) def getall(self): raise NotImplementedError def get(self, name, default=None): raise NotImplementedError pyeapi-1.0.2/pyeapi/api/acl.py0000644000076500000240000002556314447405743017337 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with EOS access control list resources This module provides an implementation for configuring and managing access access control lists on Arista EOS nodes. Access control lists can be specified as either 'standard' or 'extended' ACLs. This module provides the following class implementations: * Acls -- The top-level class used to manage both standard and extended access control lists in EOS * StandardAcls -- Class that manages the set of standard ACLs * ExtendedAcls -- Class that manages the set of extended ACLs """ import re import netaddr from pyeapi.api import EntityCollection from pyeapi.utils import ProxyCall VALID_ACLS = frozenset(['standard', 'extended']) def mask_to_prefixlen(mask): """Converts a subnet mask from dotted decimal to bit length Args: mask (str): The dotted decimal subnet mask to convert Returns: str: The subnet mask as a valid prefix length """ mask = mask or '255.255.255.255' return netaddr.IPAddress(mask).netmask_bits() def prefixlen_to_mask(prefixlen): """Converts a prefix length to a dotted decimal subnet mask Args: prefixlen (str): The prefix length value to convert Returns: str: The subt mask as a dotted decimal string """ prefixlen = prefixlen or '32' addr = '0.0.0.0/%s' % prefixlen return str(netaddr.IPNetwork(addr).netmask) class Acls(EntityCollection): def __init__(self, node, *args, **kwargs): super(Acls, self).__init__(node, *args, **kwargs) self._instances = dict() def get(self, name): return self.get_instance(name)[name] def getall(self): """Returns all ACLs in a dict object. Returns: A Python dictionary object containing all ACL configuration indexed by ACL name:: { "": {...}, "": {...} } """ acl_re = re.compile(r'^ip access-list (?:(standard) )?(.+)$', re.M) response = {'standard': {}, 'extended': {}} for acl_type, name in acl_re.findall(self.config): acl = self.get(name) if acl_type and acl_type == 'standard': response['standard'][name] = acl else: response['extended'][name] = acl return response def __getattr__(self, name): return ProxyCall(self.marshall, name) def marshall(self, name, *args, **kwargs): acl_name = args[0] acl_instance = self.get_instance(acl_name) if not hasattr(acl_instance, name): raise AttributeError("'%s' object has no attribute '%s'" % (acl_instance, name)) method = getattr(acl_instance, name) return method(*args, **kwargs) def get_instance(self, name): if name in self._instances: return self._instances[name] acl_re = re.compile(r'^ip access-list (?:(standard) )?(%s)$' % name, re.M) match = acl_re.search(self.config) if match: acl_type = match.group(1) or 'extended' return self.create_instance(match.group(2), acl_type) return {name: None} def create_instance(self, name, acl_type): if acl_type not in VALID_ACLS: acl_type = 'standard' acl_instance = ACL_CLASS_MAP.get(acl_type) self._instances[name] = acl_instance(self.node) return self._instances[name] def create(self, name, type='standard'): # Create ACL instance for ACL type Standard or Extended then call # create method for specific ACL class. acl_instance = self.create_instance(name, type) return acl_instance.create(name) class StandardAcls(EntityCollection): entry_re = re.compile(r'(\d+)' r'(?: ([p|d]\w+))' r'(?: (any))?' r'(?: (host))?' r'(?: ([0-9]+(?:\.[0-9]+){3}))?' r'(?:/([0-9]{1,2}))?' r'(?: ([0-9]+(?:\.[0-9]+){3}))?' r'(?: (log))?') def get(self, name): config = self.get_block('ip access-list standard %s' % name) if not config: return None resource = dict(name=name, type='standard') resource.update(self._parse_entries(config)) return resource def _parse_entries(self, config): entries = dict() for item in re.finditer(r'\d+ [p|d].*$', config, re.M): match = self.entry_re.match(item.group(0)) (seq, act, anyip, host, ip, mlen, mask, log) = match.groups() entry = dict() entry['action'] = act entry['srcaddr'] = ip or '0.0.0.0' entry['srclen'] = mlen or mask_to_prefixlen(mask) entry['log'] = log is not None entries[seq] = entry return dict(entries=entries) def create(self, name): return self.configure('ip access-list standard %s' % name) def delete(self, name): return self.configure('no ip access-list standard %s' % name) def default(self, name): return self.configure('default ip access-list standard %s' % name) def update_entry(self, name, seqno, action, addr, prefixlen, log=False): cmds = ['ip access-list standard %s' % name] cmds.append('no %s' % seqno) entry = '%s %s %s/%s' % (seqno, action, addr, prefixlen) if log: entry += ' log' cmds.append(entry) cmds.append('exit') return self.configure(cmds) def add_entry(self, name, action, addr, prefixlen, log=False, seqno=None): cmds = ['ip access-list standard %s' % name] entry = '%s %s/%s' % (action, addr, prefixlen) if seqno is not None: entry = '%s %s' % (seqno, entry) if log: entry += ' log' cmds.append(entry) cmds.append('exit') return self.configure(cmds) def remove_entry(self, name, seqno): cmds = ['ip access-list standard %s' % name, 'no %s' % seqno, 'exit'] return self.configure(cmds) class ExtendedAcls(EntityCollection): entry_re = re.compile(r'(\d+)' r'(?: ([p|d]\w+))' r'(?: (\w+|\d+))' r'(?: ([a|h]\w+))?' r'(?: ([0-9]+(?:\.[0-9]+){3}))?' r'(?:/([0-9]{1,2}))?' r'(?: ((?:eq|gt|lt|neq|range) [\w-]+))?' r'(?: ([a|h]\w+))?' r'(?: ([0-9]+(?:\.[0-9]+){3}))?' r'(?:/([0-9]{1,2}))?' r'(?: ([0-9]+(?:\.[0-9]+){3}))?' r'(?: ((?:eq|gt|lt|neq|range) [\w-]+))?' r'(?: (.+))?') def get(self, name): config = self.get_block('ip access-list %s' % name) if not config: return None resource = dict(name=name, type='extended') resource.update(self._parse_entries(config)) return resource def _parse_entries(self, config): entries = dict() for item in re.finditer(r'\d+ [p|d].*$', config, re.M): match = self.entry_re.match(item.group(0)) if match: entry = dict() entry['action'] = match.group(2) entry['protocol'] = match.group(3) entry['srcaddr'] = match.group(5) or 'any' entry['srclen'] = match.group(6) entry['srcport'] = match.group(7) entry['dstaddr'] = match.group(9) or 'any' entry['dstlen'] = match.group(10) entry['dstport'] = match.group(12) entry['other'] = match.group(13) entries[match.group(1)] = entry return dict(entries=entries) def create(self, name): return self.configure('ip access-list %s' % name) def delete(self, name): return self.configure('no ip access-list %s' % name) def default(self, name): return self.configure('default ip access-list %s' % name) def update_entry(self, name, seqno, action, protocol, srcaddr, srcprefixlen, dstaddr, dstprefixlen, log=False): cmds = ['ip access-list %s' % name] cmds.append('no %s' % seqno) entry = '%s %s %s %s/%s %s/%s' % (seqno, action, protocol, srcaddr, srcprefixlen, dstaddr, dstprefixlen) if log: entry += ' log' cmds.append(entry) cmds.append('exit') return self.configure(cmds) def add_entry(self, name, action, protocol, srcaddr, srcprefixlen, dstaddr, dstprefixlen, log=False, seqno=None): cmds = ['ip access-list %s' % name] entry = '%s %s %s/%s %s/%s' % (action, protocol, srcaddr, srcprefixlen, dstaddr, dstprefixlen) if seqno is not None: entry = '%s %s' % (seqno, entry) if log: entry += ' log' cmds.append(entry) cmds.append('exit') return self.configure(cmds) def remove_entry(self, name, seqno): cmds = ['ip access-list %s' % name, 'no %s' % seqno, 'exit'] return self.configure(cmds) ACL_CLASS_MAP = {'standard': StandardAcls, 'extended': ExtendedAcls} def instance(node): return Acls(node) pyeapi-1.0.2/pyeapi/api/bgp.py0000644000076500000240000003215214447405743017340 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """API module for Bgp """ import re from collections import namedtuple import netaddr from pyeapi.api import Entity, EntityCollection from pyeapi.utils import make_iterable Network = namedtuple('Network', 'prefix length route_map') class Bgp(Entity): """The Bgp class implements global BGP router configuration """ def __init__(self, *args, **kwargs): super(Bgp, self).__init__(*args, **kwargs) self._neighbors = None @property def neighbors(self): if self._neighbors is not None: return self._neighbors self._neighbors = BgpNeighbors(self.node) return self._neighbors def get(self): """Returns the bgp routing configuration as a dict object """ config = self.get_block('^router bgp .*') if not config: return None response = dict() response.update(self._parse_bgp_as(config)) response.update(self._parse_router_id(config)) response.update(self._parse_max_paths(config)) response.update(self._parse_shutdown(config)) response.update(self._parse_networks(config)) response['neighbors'] = self.neighbors.getall() return response def _parse_bgp_as(self, config): as_num = re.search(r'(?<=^router bgp ).*', config).group(0) return { 'bgp_as': int(as_num) if as_num.isnumeric() else as_num } def _parse_router_id(self, config): match = re.search(r'router-id ([^\s]+)', config) value = match.group(1) if match else None return dict(router_id=value) def _parse_max_paths(self, config): match = re.search(r'maximum-paths\s+(\d+)\s+ecmp\s+(\d+)', config) paths = int(match.group(1)) if match else None ecmp_paths = int(match.group(2)) if match else None return dict(maximum_paths=paths, maximum_ecmp_paths=ecmp_paths) def _parse_shutdown(self, config): value = 'no shutdown' in config return dict(shutdown=not value) def _parse_networks(self, config): networks = list() regexp = r'network (.+)/(\d+)(?: route-map (\w+))*' matches = re.findall(regexp, config) for (prefix, mask, rmap) in matches: rmap = None if rmap == '' else rmap networks.append(dict(prefix=prefix, masklen=mask, route_map=rmap)) return dict(networks=networks) def configure_bgp(self, cmd): config = self.get() cmds = ['router bgp {}'.format(config['bgp_as'])] cmds.extend(make_iterable(cmd)) return super(Bgp, self).configure(cmds) def create(self, bgp_as): value = int(bgp_as) if not 0 < value < 65536: raise ValueError('bgp as must be between 1 and 65535') command = 'router bgp {}'.format(bgp_as) return self.configure(command) def delete(self): config = self.get() if not config: return True command = 'no router bgp {}'.format(config['bgp_as']) return self.configure(command) def default(self): config = self.get() if not config: return True command = 'default router bgp {}'.format(config['bgp_as']) return self.configure(command) def set_router_id(self, value=None, default=False, disable=False): cmd = self.command_builder('router-id', value=value, default=default, disable=disable) return self.configure_bgp(cmd) def set_maximum_paths(self, max_path=None, max_ecmp_path=None, default=False, disable=False): if not max_path and max_ecmp_path: raise TypeError('Cannot use maximum_ecmp_paths without ' 'providing max_path') value = None if max_path: value = '{}'.format(max_path) if max_ecmp_path: value += ' ecmp {}'.format(max_ecmp_path) cmd = self.command_builder('maximum-paths', value=value, default=default, disable=disable) return self.configure_bgp(cmd) def set_shutdown(self, default=False, disable=True): # Default setting for BGP shutdown is disable=True, # meaning 'no shutdown'. # If both default and disable are false, BGP shutdown will # effectively be enabled. cmd = self.command_builder('shutdown', value=True, default=default, disable=disable) return self.configure_bgp(cmd) def add_network(self, prefix, length, route_map=None): if prefix == '' or length == '': raise ValueError('network prefix and length values ' 'may not be empty') cmd = 'network {}/{}'.format(prefix, length) if route_map: cmd += ' route-map {}'.format(route_map) return self.configure_bgp(cmd) def remove_network(self, prefix, masklen, route_map=None): if prefix == '' or masklen == '': raise ValueError('network prefix and length values ' 'may not be empty') cmd = 'no network {}/{}'.format(prefix, masklen) if route_map: cmd += ' route-map {}'.format(route_map) return self.configure_bgp(cmd) class BgpNeighbors(EntityCollection): def get(self, name): config = self.get_block('^router bgp .*') response = dict(name=name) response.update(self._parse_peer_group(config, name)) response.update(self._parse_remote_as(config, name)) response.update(self._parse_send_community(config, name)) response.update(self._parse_shutdown(config, name)) response.update(self._parse_description(config, name)) response.update(self._parse_next_hop_self(config, name)) response.update(self._parse_route_map_in(config, name)) response.update(self._parse_route_map_out(config, name)) return response def getall(self): config = self.get_block('^router bgp .*') if not config: return None collection = dict() for neighbor in re.findall(r'neighbor ([^\s]+)', config): collection[neighbor] = self.get(neighbor) return collection def _parse_peer_group(self, config, name): if self.version_number >= '4.23': regexp = r'neighbor {} peer group ([^\s]+)'.format(name) else: regexp = r'neighbor {} peer-group ([^\s]+)'.format(name) match = re.search(regexp, config) value = match.group(1) if match else None return dict(peer_group=value) def _parse_remote_as(self, config, name): remote_as_re = rf'(?<=neighbor {name} remote-as ).*' match = re.search(remote_as_re, config) return { 'remote_as': match.group(0) if match else None} def _parse_send_community(self, config, name): exp = 'no neighbor {} send-community'.format(name) value = exp in config return dict(send_community=not value) def _parse_shutdown(self, config, name): regexp = r'(?= '4.23': response = self.configure('no neighbor {} ' 'peer group'.format(name)) else: response = self.configure('no neighbor {} ' 'peer-group'.format(name)) return response def configure(self, cmd): match = re.search(r'router bgp (\d+)', self.config) if not match: raise ValueError('bgp is not configured') cmds = ['router bgp {}'.format(match.group(1)), cmd] return super(BgpNeighbors, self).configure(cmds) def command_builder(self, name, cmd, value, default, disable): string = 'neighbor {} {}'.format(name, cmd) return super(BgpNeighbors, self).command_builder(string, value, default, disable) def set_peer_group(self, name, value=None, default=False, disable=False): if not self.ispeergroup(name): if self.version_number >= '4.23': cmd = self.command_builder(name, 'peer group', value, default, disable) else: cmd = self.command_builder(name, 'peer-group', value, default, disable) return self.configure(cmd) return False def set_remote_as(self, name, value=None, default=False, disable=False): cmd = self.command_builder(name, 'remote-as', value, default, disable) return self.configure(cmd) def set_shutdown(self, name, default=False, disable=True): # Default setting for BGP neighbor shutdown is # disable=True, meaning 'no shutdown' # If both default and disable are false, BGP neighbor shutdown will # effectively be enabled. cmd = self.command_builder(name, 'shutdown', True, default, disable) return self.configure(cmd) def set_send_community(self, name, value=None, default=False, disable=False): cmd = self.command_builder(name, 'send-community', value, default, disable) return self.configure(cmd) def set_next_hop_self(self, name, value=None, default=False, disable=False): cmd = self.command_builder(name, 'next-hop-self', value, default, disable) return self.configure(cmd) def set_route_map_in(self, name, value=None, default=False, disable=False): cmd = self.command_builder(name, 'route-map', value, default, disable) cmd += ' in' return self.configure(cmd) def set_route_map_out(self, name, value=None, default=False, disable=False): cmd = self.command_builder(name, 'route-map', value, default, disable) cmd += ' out' return self.configure(cmd) def set_description(self, name, value=None, default=False, disable=False): cmd = self.command_builder(name, 'description', value, default, disable) return self.configure(cmd) def instance(api): return Bgp(api) pyeapi-1.0.2/pyeapi/api/interfaces.py0000644000076500000240000011672314447405743020722 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2017, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with interfaces in EOS This module provides an API for pragmatically working with EOS interface configurations. Interfaces include any data or management plane interface available in EOS. Parameters: name (string): The name of the interface the configuration should be applied to. The interface name is the full interface identifier. shutdown (boolean): True if the interface is administratively disabled, and False if the interface is administratively enable. This value does not validate the interfaces operational state. description (string): The interface description string. This value is an arbitrary operator defined value. sflow (boolean): True if sFlow is enabled on the interface otherwise False flowcontrol_send (string): The flowcontrol send configuration value for the interface. Valid values are on or off flowcontrol_receive (string): The flowcontrol receive configuration value for the interface. Valid values are on or off """ import re from pyeapi.api import EntityCollection from pyeapi.utils import ProxyCall, CliVariants MIN_LINKS_RE = re.compile(r'(?<=\s{3}min-links\s)(?P.+)$', re.M) DEFAULT_LACP_MODE = 'on' DEFAULT_LACP_FALLBACK = 'disabled' DEFAULT_LACP_FALLBACK_TIMEOUT = 90 VALID_INTERFACES = frozenset([ 'Ethernet', 'Management', 'Loopback', 'Port-Channel', 'Vlan', 'Vxlan', ]) def isvalidinterface(value): match = re.match(r'([EPVLM][a-z-C]+)', value) return match and match.group() in VALID_INTERFACES class Interfaces(EntityCollection): def __init__(self, node, *args, **kwargs): super(Interfaces, self).__init__(node, *args, **kwargs) self._instances = dict() def get(self, name): return self.get_instance(name)[name] def getall(self): """Returns all interfaces in a dict object. Returns: A Python dictionary object containing all interface configuration indexed by interface name:: { "Ethernet1": {...}, "Ethernet2": {...} } """ interfaces_re = re.compile(r'(?<=^interface\s)(.+)$', re.M) response = dict() for name in interfaces_re.findall(self.config): interface = self.get(name) if interface: response[name] = interface return response def __getattr__(self, name): return ProxyCall(self.marshall, name) def get_instance(self, interface): cls = INTERFACE_CLASS_MAP.get(interface[0:2]) or BaseInterface if cls in self._instances: instance = self._instances[cls] else: instance = cls(self.node) self._instances[cls] = instance return instance def marshall(self, name, *args, **kwargs): interface = args[0] if not isvalidinterface(interface): raise ValueError('invalid interface {}'.format(interface)) instance = self.get_instance(interface) if not hasattr(instance, name): raise AttributeError("'%s' object has no attribute '%s'" % (instance, name)) method = getattr(instance, name) return method(*args, **kwargs) class BaseInterface(EntityCollection): def __str__(self): return 'Interface' def get(self, name): """Returns a generic interface as a set of key/value pairs This class is should normally serve as a base class for building more specific interface resources. The attributes of this resource are common to all interfaces regardless of type in EOS. The generic interface resource returns the following: * name (str): The name of the interface * type (str): Always returns 'generic' * shutdown (bool): True if the interface is shutdown * description (str): The interface description value Args: name (str): The interface identifier to retrieve from the running-configuration Returns: A Python dictionary object of key/value pairs that represents the interface configuration. If the specified interface does not exist, then None is returned """ config = self.get_block('^interface %s' % name) if not config: return None resource = dict(name=name, type='generic') resource.update(self._parse_shutdown(config)) resource.update(self._parse_description(config)) return resource def _parse_shutdown(self, config): """Scans the specified config block and returns the shutdown value Args: config (str): The interface config block to scan Returns: dict: Returns a dict object with the shutdown value retrieved from the config block. The returned dict object is intended to be merged into the interface resource dict """ value = 'no shutdown' not in config return dict(shutdown=value) def _parse_description(self, config): """Scans the specified config block and returns the description value Args: config (str): The interface config block to scan Returns: dict: Returns a dict object with the description value retrieved from the config block. If the description value is not configured, None is returned as the value. The returned dict is intended to be merged into the interface resource dict. """ value = None match = re.search(r'description (.+)$', config, re.M) if match: value = match.group(1) return dict(description=value) def create(self, name): """Creates a new interface on the node Note: This method will attempt to create the interface regardless if the interface exists or not. If the interface already exists then this method will still return True Args: name (string): The full name of the interface. Returns: True if the interface could be created otherwise False (see Note) """ return self.configure('interface %s' % name) def delete(self, name): """Deletes the interface from the node Note: This method will attempt to delete the interface from the nodes operational config. If the interface does not exist then this method will not perform any changes but still return True Args: name (string): The full name of the interface Returns: True if the interface could be deleted otherwise False (see Node) """ return self.configure('no interface %s' % name) def default(self, name): """Defaults an interface in the running configuration Args: name (string): The full name of the interface Returns: True if the command operation succeeds otherwise False """ return self.configure('default interface %s' % name) def set_encapsulation(self, name, vid, default=False, disable=False): """Configures the subinterface encapsulation value Args: name (string): The interface identifier. It must be a full interface name (ie Ethernet, not Et) vid (int): The vlan id number default (boolean): Specifies to default the subinterface encapsulation disable (boolean): Specifies to disable the subinterface encapsulation Returns: True if the operation succeeds otherwise False is returned """ if '.' not in name: raise NotImplementedError('parameter encapsulation can only be' ' set on subinterfaces') if name[0:2] not in ['Et', 'Po']: raise NotImplementedError('parameter encapsulation can only be' ' set on Ethernet and Port-Channel' ' subinterfaces') commands = ['interface %s' % name] commands.append(self.command_builder('encapsulation dot1q vlan', str(vid), default=default, disable=disable)) return self.configure(commands) def set_description(self, name, value=None, default=False, disable=False): """Configures the interface description EosVersion: 4.13.7M Args: name (string): The interface identifier. It must be a full interface name (ie Ethernet, not Et) value (string): The value to set the description to. default (boolean): Specifies to default the interface description disable (boolean): Specifies to negate the interface description Returns: True if the operation succeeds otherwise False """ string = 'description' commands = self.command_builder(string, value=value, default=default, disable=disable) return self.configure_interface(name, commands) def set_shutdown(self, name, default=False, disable=True): """Configures the interface shutdown state Default configuration for set_shutdown is disable=True, meaning 'no shutdown'. Setting both default and disable to False will effectively enable shutdown on the interface. Args: name (string): The interface identifier. It must be a full interface name (ie Ethernet, not Et) default (boolean): Specifies to default the interface shutdown disable (boolean): Specifies to disable interface shutdown, i.e. disable=True => no shutdown Returns: True if the operation succeeds otherwise False is returned """ commands = ['interface %s' % name] commands.append(self.command_builder('shutdown', value=True, default=default, disable=disable)) return self.configure(commands) class EthernetInterface(BaseInterface): def __str__(self): return 'EthernetInterface' def get(self, name): """Returns an interface as a set of key/value pairs Args: name (string): the interface identifier to retrieve the from the configuration Returns: A Python dictionary object of key/value pairs that represent the current configuration for the specified node. If the specified interface name does not exist, then None is returned:: { "name": , "type": "ethernet", "sflow": [true, false], "flowcontrol_send": [on, off], "flowcontrol_receive": [on, off] } """ config = self.get_block('^interface %s' % name) if not config: return None resource = super(EthernetInterface, self).get(name) resource.update(dict(name=name, type='ethernet')) resource.update(self._parse_sflow(config)) resource.update(self._parse_flowcontrol_send(config)) resource.update(self._parse_flowcontrol_receive(config)) return resource def _parse_sflow(self, config): """Scans the specified config block and returns the sflow value Args: config (str): The interface config block to scan Returns: dict: Returns a dict object with the sflow value retrieved from the config block. The returned dict object is intended to be merged into the interface resource dict """ value = 'no sflow' not in config return dict(sflow=value) def _parse_flowcontrol_send(self, config): """Scans the config block and returns the flowcontrol send value Args: config (str): The interface config block to scan Returns: dict: Returns a dict object with the flowcontrol send value retrieved from the config block. The returned dict object is intended to be merged into the interface resource dict """ value = 'off' match = re.search(r'flowcontrol send (\w+)$', config, re.M) if match: value = match.group(1) return dict(flowcontrol_send=value) def _parse_flowcontrol_receive(self, config): """Scans the config block and returns the flowcontrol receive value Args: config (str): The interface config block to scan Returns: dict: Returns a dict object with the flowcontrol receive value retrieved from the config block. The returned dict object is intended to be merged into the interface resource dict """ value = 'off' match = re.search(r'flowcontrol receive (\w+)$', config, re.M) if match: value = match.group(1) return dict(flowcontrol_receive=value) def create(self, name): """Create an Ethernet subinterface Args: name (string): The subinterface name. Ex: Ethernet1.1 Raises: NotImplementedError: creating physical Ethernet interfaces is not supported. Only subinterfaces can be created. """ if '.' not in name: raise NotImplementedError('creating physical Ethernet interfaces' ' is not supported. Only subinterfaces' ' can be created') return self.configure(['interface %s' % name]) def delete(self, name): """Delete an Ethernet subinterfaces Args: name (string): The subinterface name. Ex: Ethernet1.1 Raises: NotImplementedError: creating physical Ethernet interfaces is not supported. Only subinterfaces can be created. """ if '.' not in name: raise NotImplementedError('deleting physical Ethernet interfaces' ' is not supported. Only subinterfaces' ' can be created') return self.configure(['no interface %s' % name]) def set_flowcontrol_send(self, name, value=None, default=False, disable=False): """Configures the interface flowcontrol send value Args: name (string): The interface identifier. It must be a full interface name (ie Ethernet, not Et) value (boolean): True if the interface should enable sending flow control packets, otherwise False default (boolean): Specifies to default the interface flow control send value disable (boolean): Specifies to disable the interface flow control send value Returns: True if the operation succeeds otherwise False is returned """ return self.set_flowcontrol(name, 'send', value, default, disable) def set_flowcontrol_receive(self, name, value=None, default=False, disable=False): """Configures the interface flowcontrol receive value Args: name (string): The interface identifier. It must be a full interface name (ie Ethernet, not Et) value (boolean): True if the interface should enable receiving flow control packets, otherwise False default (boolean): Specifies to default the interface flow control receive value disable (boolean): Specifies to disable the interface flow control receive value Returns: True if the operation succeeds otherwise False is returned """ return self.set_flowcontrol(name, 'receive', value, default, disable) def set_flowcontrol(self, name, direction, value=None, default=False, disable=False): """Configures the interface flowcontrol value Args: name (string): The interface identifier. It must be a full interface name (ie Ethernet, not Et) direction (string): one of either 'send' or 'receive' value (boolean): True if the interface should enable flow control packet handling, otherwise False default (boolean): Specifies to default the interface flow control send or receive value disable (boolean): Specifies to disable the interface flow control send or receive value Returns: True if the operation succeeds otherwise False is returned """ if value is not None: if value not in ['on', 'off']: raise ValueError('invalid flowcontrol value') if direction not in ['send', 'receive']: raise ValueError('invalid direction specified') commands = ['interface %s' % name] commands.append(self.command_builder('flowcontrol %s' % direction, value=value, default=default, disable=disable)) return self.configure(commands) def set_sflow(self, name, value=None, default=False, disable=False): """Configures the sFlow state on the interface Args: name (string): The interface identifier. It must be a full interface name (ie Ethernet, not Et) value (boolean): True if sFlow should be enabled otherwise False default (boolean): Specifies the default value for sFlow disable (boolean): Specifies to disable sFlow Returns: True if the operation succeeds otherwise False is returned """ if value not in [True, False, None]: raise ValueError commands = ['interface %s' % name] commands.append(self.command_builder('sflow enable', value=value, default=default, disable=disable)) return self.configure(commands) def set_vrf(self, name, vrf, default=False, disable=False): """Applies a VRF to the interface Note: VRF being applied to interface must already exist in switch config. Ethernet port must be in routed mode. This functionality can also be handled in the VRF api. Args: name (str): The interface identifier. It must be a full interface name (ie Ethernet, not Et) vrf (str): The vrf name to be applied to the interface default (bool): Specifies the default value for VRF disable (bool): Specifies to disable VRF Returns: True if the operation succeeds otherwise False is returned """ commands = ['interface %s' % name] if self.version_number >= '4.23': commands.append(self.command_builder('vrf', vrf, default=default, disable=disable)) else: commands.append(self.command_builder('vrf forwarding', vrf, default=default, disable=disable)) return self.configure(commands) class PortchannelInterface(BaseInterface): def __str__(self): return 'PortchannelInterface' def get(self, name): """Returns a Port-Channel interface as a set of key/value pairs Args: name (str): The interface identifier to retrieve from the running-configuration Returns: A Python dictionary object of key/value pairs that represents the interface configuration. If the specified interface does not exist, then None is returned:: { "name": , "type": "portchannel", "members": , "minimum_links: , "lacp_mode": [on, active, passive] } """ config = self.get_block('^interface %s' % name) if not config: return None response = super(PortchannelInterface, self).get(name) response.update(dict(name=name, type='portchannel')) response['members'] = self.get_members(name) response['lacp_mode'] = self.get_lacp_mode(name) response.update(self._parse_minimum_links(config)) response.update(self._parse_lacp_timeout(config)) response.update(self._parse_lacp_fallback(config)) return response def _parse_minimum_links(self, config): value = 0 match = re.search(r'port-channel min-links (\d+)', config) if match: value = int(match.group(1)) return dict(minimum_links=value) def _parse_lacp_fallback(self, config): value = DEFAULT_LACP_FALLBACK match = re.search(r'lacp fallback (static|individual)', config) if match: value = match.group(1) return dict(lacp_fallback=value) def _parse_lacp_timeout(self, config): value = DEFAULT_LACP_FALLBACK_TIMEOUT match = re.search(r'lacp fallback timeout (\d+)', config) if match: value = int(match.group(1)) return dict(lacp_timeout=value) def get_lacp_mode(self, name): """Returns the LACP mode for the specified Port-Channel interface Args: name(str): The Port-Channel interface name to return the LACP mode for from the configuration Returns: The configured LACP mode for the interface. Valid mode values are 'on', 'passive', 'active' """ members = self.get_members(name) if not members: return DEFAULT_LACP_MODE for member in self.get_members(name): match = re.search(r'channel-group\s\d+\smode\s(?P.+)', self.get_block('^interface %s' % member)) return match.group('value') def get_members(self, name): """Returns the member interfaces for the specified Port-Channel Args: name(str): The Port-channel interface name to return the member interfaces for Returns: A list of physical interface names that belong to the specified interface """ grpid = re.search(r'(\d+)', name).group() command = 'show port-channel %s all-ports' % grpid config = self.node.enable(command, 'text') return re.findall(r'\b(?!Peer)Ethernet[\d/]*\b', config[0]['result']['output']) def set_members(self, name, members, mode=None): """Configures the array of member interfaces for the Port-Channel Args: name(str): The Port-Channel interface name to configure the member interfaces members(list): The list of Ethernet interfaces that should be member interfaces mode(str): The LACP mode to configure the member interfaces to. Valid values are 'on, 'passive', 'active'. When there are existing channel-group members and their lacp mode differs from this attribute, all of those members will be removed and then re-added using the specified lacp mode. If this attribute is omitted, the existing lacp mode will be used for new member additions. Returns: True if the operation succeeds otherwise False """ commands = list() grpid = re.search(r'(\d+)', name).group() current_members = self.get_members(name) lacp_mode = self.get_lacp_mode(name) if mode and mode != lacp_mode: lacp_mode = mode self.set_lacp_mode(grpid, lacp_mode) # remove members from the current port-channel interface for member in set(current_members).difference(members): commands.append('interface %s' % member) commands.append('no channel-group %s' % grpid) # add new member interfaces to the port-channel interface for member in set(members).difference(current_members): commands.append('interface %s' % member) commands.append('channel-group %s mode %s' % (grpid, lacp_mode)) return self.configure(commands) if commands else True def set_lacp_mode(self, name, mode): """Configures the LACP mode of the member interfaces Args: name(str): The Port-Channel interface name to configure the LACP mode mode(str): The LACP mode to configure the member interfaces to. Valid values are 'on, 'passive', 'active' Returns: True if the operation succeeds otherwise False """ if mode not in ['on', 'passive', 'active']: return False grpid = re.search(r'(\d+)', name).group() remove_commands = list() add_commands = list() for member in self.get_members(name): remove_commands.append('interface %s' % member) remove_commands.append('no channel-group %s' % grpid) add_commands.append('interface %s' % member) add_commands.append('channel-group %s mode %s' % (grpid, mode)) return self.configure(remove_commands + add_commands) def set_minimum_links(self, name, value=None, default=False, disable=False): """Configures the Port-Channel min-links value Args: name(str): The Port-Channel interface name value(str): The value to configure the min-links default (bool): Specifies to default the port channel min-links value disable (bool): Specifies to disable the port channel min-links value Returns: True if the operation succeeds otherwise False is returned """ commands = ['interface %s' % name] commands.append(self.command_builder('port-channel min-links', value=value, default=default, disable=disable)) return self.configure(commands) def set_lacp_fallback(self, name, mode=None): """Configures the Port-Channel lacp_fallback Args: name(str): The Port-Channel interface name mode(str): The Port-Channel LACP fallback setting Valid values are 'disabled', 'static', 'individual': * static - Fallback to static LAG mode * individual - Fallback to individual ports * disabled - Disable LACP fallback Returns: True if the operation succeeds otherwise False is returned """ if mode not in ['disabled', 'static', 'individual']: return False disable = True if mode == 'disabled' else False commands = ['interface %s' % name] commands.append(self.command_builder('port-channel lacp fallback', value=mode, disable=disable)) return self.configure(commands) def set_lacp_timeout(self, name, value=None): """Configures the Port-Channel LACP fallback timeout The fallback timeout configures the period an interface in fallback mode remains in LACP mode without receiving a PDU. Args: name(str): The Port-Channel interface name value(int): port-channel lacp fallback timeout in seconds Returns: True if the operation succeeds otherwise False is returned """ commands = ['interface %s' % name] string = 'port-channel lacp fallback timeout' commands.append(self.command_builder(string, value=value)) return self.configure(commands) class VxlanInterface(BaseInterface): DEFAULT_SRC_INTF = '' DEFAULT_MCAST_GRP = '' def __str__(self): return 'VxlanInterface' def get(self, name): """Returns a Vxlan interface as a set of key/value pairs The Vxlan interface resource returns the following: * name (str): The name of the interface * type (str): Always returns 'vxlan' * source_interface (str): The vxlan source-interface value * multicast_group (str): The vxlan multicast-group value * udp_port (int): The vxlan udp-port value * vlans (dict): The vlan to vni mappings * flood_list (list): The list of global VTEP flood list * multicast_decap (bool): If the multicast decap feature is configured Args: name (str): The interface identifier to retrieve from the running-configuration Returns: A Python dictionary object of key/value pairs that represents the interface configuration. If the specified interface does not exist, then None is returned """ config = self.get_block('^interface %s' % name) if not config: return None response = super(VxlanInterface, self).get(name) response.update(dict(name=name, type='vxlan')) response.update(self._parse_source_interface(config)) response.update(self._parse_multicast_group(config)) response.update(self._parse_udp_port(config)) response.update(self._parse_vlans(config)) response.update(self._parse_flood_list(config)) response.update(self._parse_multicast_decap(config)) return response def _parse_source_interface(self, config): """ Parses the conf block and returns the vxlan source-interface value Parses the provided configuration block and returns the value of vxlan source-interface. If the value is not configured, this method will return DEFAULT_SRC_INTF instead. Args: config (str): The Vxlan config block to scan Return: dict: A dict object intended to be merged into the resource dict """ match = re.search(r'vxlan source-interface ([^\s]+)', config) value = match.group(1) if match else self.DEFAULT_SRC_INTF return dict(source_interface=value) def _parse_multicast_group(self, config): match = re.search(r'vxlan multicast-group ' r'([\d]{3}\.[\d]+\.[\d]+\.[\d]+)', config) value = match.group(1) if match else self.DEFAULT_MCAST_GRP return dict(multicast_group=value) def _parse_multicast_decap(self, config): val1 = 'vxlan multicast-group decap' in config val2 = 'no vxlan multicast-group decap' in config return dict( multicast_decap=bool(val1 ^ val2) ) def _parse_udp_port(self, config): match = re.search(r'vxlan udp-port (\d+)', config) value = int(match.group(1)) return dict(udp_port=value) def _parse_vlans(self, config): vlans = frozenset(re.findall(r'vxlan vlan (\d+)', config)) values = dict() for vid in vlans: values[vid] = dict() regexp = r'vxlan vlan {} vni (\d+)'.format(vid) match = re.search(regexp, config) values[vid]['vni'] = match.group(1) if match else None regexp = r'vxlan vlan {} flood vtep (.*)$'.format(vid) matches = re.search(regexp, config, re.M) flood_list = matches.group(1).split(' ') if matches else [] values[vid]['flood_list'] = flood_list return dict(vlans=values) def _parse_flood_list(self, config): match = re.search(r'^ *vxlan flood vtep +([\d. ]+)$', config, re.M) values = list() if match: values = match.group(1).split(' ') return dict(flood_list=values) def set_source_interface(self, name, value=None, default=False, disable=False): """Configures the Vxlan source-interface value EosVersion: 4.13.7M Args: name(str): The interface identifier to configure, defaults to Vxlan1 value(str): The value to configure the source-interface to default(bool): Configures the source-interface value to default disable(bool): Negates the source-interface Returns: True if the operation succeeds otherwise False """ string = 'vxlan source-interface' cmds = self.command_builder(string, value=value, default=default, disable=disable) return self.configure_interface(name, cmds) def set_multicast_group(self, name, value=None, default=False, disable=False): """Configures the Vxlan multicast-group value EosVersion: 4.13.7M Args: name(str): The interface identifier to configure, defaults to Vxlan1 value(str): The value to configure the multicast-group to default(bool): Configures the mulitcast-group value to default disable(bool): Negates the multicast-group value Returns: True if the operation succeeds otherwise False """ string_dpr = 'vxlan multicast-group' cmds_dpr = self.command_builder(string_dpr, value=value, default=default, disable=disable) string_new = 'vxlan multicast-group decap' cmds_new = self.command_builder(string_new, value=value, default=default, disable=disable) return self.configure_interface(name, CliVariants(cmds_new, cmds_dpr) ) def set_multicast_decap(self, name, default=False, disable=False): """Configures the Vxlan multicast-group decap feature EosVersion: 4.15.0M Args: name(str): The interface identifier to configure, defaults to Vxlan1 default(bool): Configures the mulitcast-group decap value to default disable(bool): Negates the multicast-group decap value Returns: True if the operation succeeds otherwise False """ string = 'vxlan multicast-group decap' if default or disable: cmds = self.command_builder(string, value=None, default=default, disable=disable) else: cmds = [string] return self.configure_interface(name, cmds) def set_udp_port(self, name, value=None, default=False, disable=False): """Configures vxlan udp-port value EosVersion: 4.13.7M Args: name(str): The name of the interface to configure value(str): The value to set udp-port to default(bool): Configure using the default keyword disable(bool): Negate the udp-port value Returns: True if the operation succeeds otherwise False """ string = 'vxlan udp-port' cmds = self.command_builder(string, value=value, default=default, disable=disable) return self.configure_interface(name, cmds) def add_vtep(self, name, vtep, vlan=None): """Adds a new VTEP endpoint to the global or local flood list EosVersion: 4.13.7M Args: name (str): The name of the interface to configure vtep (str): The IP address of the remote VTEP endpoint to add vlan (str): The VLAN ID associated with this VTEP. If the VLAN keyword is used, then the VTEP is configured as a local flood endpoing Returns: True if the command completes successfully """ if not vlan: cmd = 'vxlan flood vtep add {}'.format(vtep) else: cmd = 'vxlan vlan {} flood vtep add {}'.format(vlan, vtep) return self.configure_interface(name, cmd) def remove_vtep(self, name, vtep, vlan=None): """Removes a VTEP endpoint from the global or local flood list EosVersion: 4.13.7M Args: name (str): The name of the interface to configure vtep (str): The IP address of the remote VTEP endpoint to add vlan (str): The VLAN ID associated with this VTEP. If the VLAN keyword is used, then the VTEP is configured as a local flood endpoing Returns: True if the command completes successfully """ if not vlan: cmd = 'vxlan flood vtep remove {}'.format(vtep) else: cmd = 'vxlan vlan {} flood vtep remove {}'.format(vlan, vtep) return self.configure_interface(name, cmd) def update_vlan(self, name, vid, vni): """Adds a new vlan to vni mapping for the interface EosVersion: 4.13.7M Args: vlan (str, int): The vlan id to map to the vni vni (str, int): The vni value to use Returns: True if the command completes successfully """ cmd = 'vxlan vlan add %s vni %s' % (vid, vni) return self.configure_interface(name, cmd) def remove_vlan(self, name, vid): """Removes a vlan to vni mapping for the interface EosVersion: 4.13.7M Args: vlan (str, int): The vlan id to map to the vni Returns: True if the command completes successfully """ return self.configure_interface( name, CliVariants(f'vxlan vlan remove {vid} vni $', f'vxlan vlan remove {vid} vni') ) INTERFACE_CLASS_MAP = { 'Et': EthernetInterface, 'Po': PortchannelInterface, 'Vx': VxlanInterface } def instance(api): return Interfaces(api) pyeapi-1.0.2/pyeapi/api/ipinterfaces.py0000644000076500000240000002365514447405743021254 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working the logical IP interfaces in EOS This module provides an API for configuring logical IP interfaces using EOS and eAPI. Parameters: name (string): The interface name the configuration is in reference to. The interface name is the full interface identifier. address (string): The interface IP address in the form of address/len. mtu (integer): The interface MTU value. The MTU value accepts integers in the range of 68 to 65535 bytes. See RFC 791 and RFC 8200 for more information. """ import re from pyeapi.api import EntityCollection from pyeapi.utils import _interpolate_docstr IP_MTU_MIN = 68 IP_MTU_MAX = 65535 SWITCHPORT_RE = re.compile(r'no switchport$', re.M) class Ipinterfaces( EntityCollection ): def get( self, name ): """Returns the specific IP interface properties The Ipinterface resource returns the following: * name (str): The name of the interface * address (str): The IP address of the interface in the form of A.B.C.D/E (None if no ip configured) * secondary (list): The list of secondary IP addresses of the interface (if any configured) * mtu (int): The configured value for IP MTU. Args: name (string): The interface identifier to retrieve the configuration for Return: A Python dictionary object of key/value pairs that represents the current configuration of the node. If the specified interface does not exist then None is returned. """ config = self.get_block( 'interface %s' % name ) if name[ 0:2 ] in [ 'Et', 'Po' ] and not SWITCHPORT_RE.search( config, re.M ): return None resource = dict( name=name ) resource.update( self._parse_address(config) ) resource.update( self._parse_mtu(config) ) return resource def _parse_address( self, config ): """Parses the config block and returns the ip address value The provided configuration block is scanned and the configured value for the IP address is returned as a dict object. If the IP address value is not configured, then None is returned for the value Args: config (str): The interface configuration block to parse Return: dict: A dict object intended to be merged into the resource dict """ match = re.findall( r'ip address ([^\s]+)', config, re.M ) primary, secondary = ( match[0], match[1:] ) if match else ( None, None ) return dict( address=primary, secondary=secondary ) if secondary else dict( address=primary ) def _parse_mtu(self, config): """Parses the config block and returns the configured IP MTU value The provided configuration block is scanned and the configured value for the IP MTU is returned as a dict object. The IP MTU value is expected to always be present in the provided config block Args: config (str): The interface configuration block to parse Return: dict: A dict object intended to be merged into the resource dict """ match = re.search(r'mtu (\d+)', config) return dict( mtu=int(match.group( 1 )) if match else None ) def getall(self): """ Returns all of the IP interfaces found in the running-config Returns: A Python dictionary object of key/value pairs keyed by interface name that represents all of the IP interfaces on the current node:: { 'Ethernet1': {...}, 'Ethernet2': {...} } """ interfaces_re = re.compile(r'^interface\s(.+)', re.M) response = dict() for name in interfaces_re.findall(self.config): interface = self.get(name) if interface: response[name] = interface return response def create(self, name): """ Creates a new IP interface instance This method will create a new logical IP interface for the specified physical interface. If a logical IP interface already exists then this operation will have no effect. Note: Configuring a logical IP interface on a physical interface will remove any existing logical switchports have have been created Args: name (string): The interface identifier to create the logical layer 3 IP interface for. The name must be the full interface name and not an abbreviated interface name (eg Ethernet1, not Et1). Returns: True if the create operation succeeds otherwise False. If the specified interface is already created the this method will have no effect but will still return True """ commands = ['interface %s' % name, 'no switchport'] return self.configure(commands) def delete(self, name): """ Deletes an IP interface instance from the running configuration This method will delete the logical IP interface for the specified physical interface. If the interface does not have a logical IP interface defined, then this method will have no effect. Args: name (string): The interface identifier to create the logical layer 3 IP interface for. The name must be the full interface name and not an abbreviated interface name (eg Ethernet1, not Et1). Returns: True if the delete operation succeeds otherwise False. """ commands = ['interface %s' % name, 'no ip address', 'switchport'] return self.configure(commands) def set_address(self, name, value=None, default=False, disable=False): """ Configures the interface IP address Args: name (string): The interface identifier to apply the interface config to value (string): The IP address and mask to set the interface to. The value should be in the format of A.B.C.D/E default (bool): Configures the address parameter to its default value using the EOS CLI default command disable (bool): Negates the address parameter value using the EOS CLI no command Returns: True if the operation succeeds otherwise False. """ commands = ['interface %s' % name] commands.append(self.command_builder('ip address', value=value, default=default, disable=disable)) return self.configure(commands) @_interpolate_docstr( 'IP_MTU_MIN', 'IP_MTU_MAX' ) def set_mtu(self, name, value=None, default=False, disable=False): """ Configures the interface IP MTU Args: name (string): The interface identifier to apply the interface config to value (integer): The MTU value to set the interface to. Accepted values include IP_MTU_MIN to IP_MTU_MAX default (bool): Configures the mtu parameter to its default value using the EOS CLI default command disable (bool); Negate the mtu parameter value using the EOS CLI no command Returns: True if the operation succeeds otherwise False. Raises: ValueError: If the value for MTU is not an integer value or outside of the allowable range """ if value is not None: value = int(value) if not IP_MTU_MIN <= value <= IP_MTU_MAX: raise ValueError('invalid mtu value') commands = ['interface %s' % name] commands.append(self.command_builder('mtu', value=value, default=default, disable=disable)) return self.configure(commands) def instance(node): """Returns an instance of Ipinterfaces This method will create and return an instance of the Ipinterfaces object passing the value of node to the instance. This function is required for the resource to be autoloaded by the Node object Args: node (Node): The node argument provides an instance of Node to the Ipinterfaces instance """ return Ipinterfaces(node) pyeapi-1.0.2/pyeapi/api/mlag.py0000644000076500000240000002533414447405743017514 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with EOS MLAG resources The Mlag resource provides configuration management of global and interface MLAG settings on an EOS node. Parameters: config (dict): The global MLAG configuration values interfaces (dict): The configured MLAG interfaces Config Parameters: domain_id (str): The domain_id parameter is parsed from the nodes mlag configuration. The domain id is an alphanumeric string that names the MLAG domain local_interface (str): The local VLAN interface used as the control plane endpoint between MLAG peers. Valid values include any VLAN SVI peer_address (str): The IP address of the MLAG peer used to send MLAG control traffic. The peer address must be reachable from the local interface. Valid values include any IPv4 unicast address peer_link (str): The physical link that connects the node to its MLAG peer. Valid values for the peer link include layer 2 Ethernet or Port-Channel interfaces shutdown (bool): The administrative state of the global MLAG process. Interface Parameters: mlag_id (str): The interface mlag parameter parsed from the nodes interface configuration. Valid values for the mlag id are in the range of 1 to 2000 """ import re from pyeapi.api import Entity class Mlag(Entity): """The Mlag class provides management of the MLAG configuration The Mlag class is derived from Entity and provides an API for working with the nodes MLAG configuraiton. """ def get(self): """Returns the Mlag configuration as a resource dict Returns: dict: A dict ojbect containing the Mlag resource attributes. """ resource = dict() resource.update(self._parse_config()) resource.update(self._parse_interfaces()) return resource def _parse_config(self): """Parses the mlag global configuration Returns: dict: A dict object that is intended to be merged into the resource dict """ config = self.get_block('mlag configuration') cfg = dict() cfg.update(self._parse_domain_id(config)) cfg.update(self._parse_local_interface(config)) cfg.update(self._parse_peer_address(config)) cfg.update(self._parse_peer_link(config)) cfg.update(self._parse_shutdown(config)) return dict(config=cfg) def _parse_domain_id(self, config): """Scans the config block and parses the domain-id value Args: config (str): The config block to scan Returns: dict: A dict object that is intended to be merged into the resource dict """ match = re.search(r'domain-id (.+)$', config) value = match.group(1) if match else None return dict(domain_id=value) def _parse_local_interface(self, config): """Scans the config block and parses the local-interface value Args: config (str): The config block to scan Returns: dict: A dict object that is intended to be merged into the resource dict """ match = re.search(r'local-interface (\w+)', config) value = match.group(1) if match else None return dict(local_interface=value) def _parse_peer_address(self, config): """Scans the config block and parses the peer-address value Args: config (str): The config block to scan Returns: dict: A dict object that is intended to be merged into the resource dict """ match = re.search(r'peer-address (\d+\.\d+\.\d+\.\d+)$', config) value = match.group(1) if match else None return dict(peer_address=value) def _parse_peer_link(self, config): """Scans the config block and parses the peer-link value Args: config (str): The config block to scan Returns: dict: A dict object that is intended to be merged into the resource dict """ match = re.search(r'peer-link (\S+)', config) value = match.group(1) if match else None return dict(peer_link=value) def _parse_shutdown(self, config): """Scans the config block and parses the shutdown value Args: config (str): The config block to scan Returns: dict: A dict object that is intended to be merged into the resource dict """ value = 'no shutdown' not in config return dict(shutdown=value) def _parse_interfaces(self): """Scans the global config and returns the configured interfaces Returns: dict: A dict object that is intended to be merged into the resource dict. """ interfaces = dict() names = re.findall(r'^interface (Po.+)$', self.config, re.M) for name in names: config = self.get_block('interface %s' % name) match = re.search(r'mlag (\d+)', config) if match: interfaces[name] = dict(mlag_id=match.group(1)) return dict(interfaces=interfaces) def _configure_mlag(self, string, value, default, disable): cfg = self.command_builder(string, value=value, default=default, disable=disable) cmds = ['mlag configuration', cfg] return super(Mlag, self).configure(cmds) def set_domain_id(self, value=None, default=False, disable=False): """Configures the mlag domain-id value Args: value (str): The value to configure the domain-id default (bool): Configures the domain-id using the default keyword disable (bool): Negates the domain-id using the no keyword Returns: bool: Returns True if the commands complete successfully """ return self._configure_mlag('domain-id', value, default, disable) def set_local_interface(self, value=None, default=False, disable=False): """Configures the mlag local-interface value Args: value (str): The value to configure the local-interface default (bool): Configures the local-interface using the default keyword disable (bool): Negates the local-interface using the no keyword Returns: bool: Returns True if the commands complete successfully """ return self._configure_mlag('local-interface', value, default, disable) def set_peer_address(self, value=None, default=False, disable=False): """Configures the mlag peer-address value Args: value (str): The value to configure the peer-address default (bool): Configures the peer-address using the default keyword disable (bool): Negates the peer-address using the no keyword Returns: bool: Returns True if the commands complete successfully """ return self._configure_mlag('peer-address', value, default, disable) def set_peer_link(self, value=None, default=False, disable=False): """Configures the mlag peer-link value Args: value (str): The value to configure the peer-link default (bool): Configures the peer-link using the default keyword disable (bool): Negates the peer-link using the no keyword Returns: bool: Returns True if the commands complete successfully """ return self._configure_mlag('peer-link', value, default, disable) def set_shutdown(self, default=False, disable=True): """Configures the mlag shutdown value Default setting for set_shutdown is disable=True, meaning 'no shutdown'. Setting both default and disable to False will effectively enable shutdown. Args: default (bool): Configures the shutdown using the default keyword disable (bool): Negates shutdown using the no keyword Returns: bool: Returns True if the commands complete successfully """ return self._configure_mlag('shutdown', True, default, disable) def set_mlag_id(self, name, value=None, default=False, disable=False): """Configures the interface mlag value for the specified interface Args: name (str): The interface to configure. Valid values for the name arg include Port-Channel* value (str): The mlag identifier to cofigure on the interface default (bool): Configures the interface mlag value using the default keyword disable (bool): Negates the interface mlag value using the no keyword Returns: bool: Returns True if the commands complete successfully """ cmd = self.command_builder('mlag', value=value, default=default, disable=disable) return self.configure_interface(name, cmd) def instance(node): """Returns an instance of Mlag Args: node (Node): The node argument passes an instance of Node to the resource Returns: object: An instance of Mlag """ return Mlag(node) pyeapi-1.0.2/pyeapi/api/ntp.py0000644000076500000240000001613514447405743017374 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for managing the NTP configuration in EOS This module provides an API for configuring NTP resources using EOS and eAPI. Arguments: name (string): The interface port that specifies the NTP source. """ import re from pyeapi.api import Entity class Ntp(Entity): """The Ntp class implements global NTP router configuration """ def __init__(self, *args, **kwargs): super(Ntp, self).__init__(*args, **kwargs) def get(self): """Returns the current NTP configuration The Ntp resource returns the following: * source_interface (str): The interface port that specifies NTP server * servers (list): A list of the NTP servers that have been assigned to the node. Each entry in the list is a key/value pair of the name of the server as the key and None or 'prefer' as the value if the server is preferred. Returns: A Python dictionary object of key/value pairs that represents the current NTP configuration of the node:: { "source_interface": 'Loopback0', 'servers': [ { '1.1.1.1': None }, { '1.1.1.2': 'prefer' }, { '1.1.1.3': 'prefer' }, { '1.1.1.4': None }, ] } """ config = self.config if not config: return None response = dict() response.update(self._parse_source_interface(config)) response.update(self._parse_servers(config)) return response def _parse_source_interface(self, config): if self.version_number >= '4.23': match = re.search(r'^ntp local-interface ([^\s]+)', config, re.M) else: match = re.search(r'^ntp source ([^\s]+)', config, re.M) value = match.group(1) if match else None return dict(source_interface=value) def _parse_servers(self, config): matches = re.findall(r'ntp server ([\S]+) ?(prefer)?', config, re.M) value = [] for match in matches: server = match[0] prefer = match[1] if match[1] == 'prefer' else None value.append({server: prefer}) return dict(servers=value) def create(self, name): """Instantiate the NTP by setting the source interface. Args: name (string): The interface port that specifies the NTP source. Returns: True if the operation succeeds, otherwise False. """ return self.set_source_interface(name) def delete(self): """Delete the NTP source entry from the node. Returns: True if the operation succeeds, otherwise False. """ if self.version_number >= '4.23': cmd = self.command_builder('ntp local-interface', disable=True) else: cmd = self.command_builder('ntp source', disable=True) return self.configure(cmd) def default(self): """Default the NTP source entry from the node. Returns: True if the operation succeeds, otherwise False. """ if self.version_number >= '4.23': cmd = self.command_builder('ntp local-interface', default=True) else: cmd = self.command_builder('ntp source', default=True) return self.configure(cmd) def set_source_interface(self, name): """Assign the NTP source on the node Args: name (string): The interface port that specifies the NTP source. Returns: True if the operation succeeds, otherwise False. """ if self.version_number >= '4.23': cmd = self.command_builder('ntp local-interface', value=name) else: cmd = self.command_builder('ntp source', value=name) return self.configure(cmd) def add_server(self, name, prefer=False): """Add or update an NTP server entry to the node config Args: name (string): The IP address or FQDN of the NTP server. prefer (bool): Sets the NTP server entry as preferred if True. Returns: True if the operation succeeds, otherwise False. """ if not name or re.match(r'^[\s]+$', name): raise ValueError('ntp server name must be specified') if prefer: name = '%s prefer' % name cmd = self.command_builder('ntp server', value=name) return self.configure(cmd) def remove_server(self, name): """Remove an NTP server entry from the node config Args: name (string): The IP address or FQDN of the NTP server. Returns: True if the operation succeeds, otherwise False. """ cmd = self.command_builder('no ntp server', value=name) return self.configure(cmd) def remove_all_servers(self): """Remove all NTP server entries from the node config Returns: True if the operation succeeds, otherwise False. """ # 'no ntp' removes all server entries. # For command_builder, disable command 'ntp' gives the desired command cmd = self.command_builder('ntp', disable=True) return self.configure(cmd) def instance(node): """Returns an instance of Ntp Args: node (Node): The node argument passes an instance of Node to the resource Returns: object: An instance of Ntp """ return Ntp(node) pyeapi-1.0.2/pyeapi/api/ospf.py0000644000076500000240000003175314447405743017545 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2016, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with OSPF configuration in EOS This module provides an API for creating/modifying/deleting OSPF configurations """ import re from pyeapi.api import Entity from pyeapi.utils import make_iterable class Ospf(Entity): """ The Ospf class implements global Ospf router configuration """ def __init__(self, *args, **kwargs): super(Ospf, self).__init__(*args, **kwargs) pass def get(self, vrf=None): """Returns the OSPF routing configuration Args: vrf (str): VRF name to return OSPF routing config for Returns: dict: keys: router_id (int): OSPF router-id vrf (str): VRF of the OSPF process networks (dict): All networks that are advertised in OSPF ospf_process_id (int): OSPF proc id redistribution (dict): All protocols that are configured to be redistributed in OSPF shutdown (bool): Gives the current shutdown off the process """ match = '^router ospf .*' if vrf: match += ' vrf %s' % vrf config = self.get_block(match) if not config: return None response = dict() response.update(self._parse_router_id(config)) response.update(self._parse_vrf(config)) response.update(self._parse_networks(config)) response.update(self._parse_ospf_process_id(config)) response.update(self._parse_redistribution(config)) response.update(self._parse_shutdown(config)) return response def _parse_ospf_process_id(self, config): """Parses config file for the OSPF proc ID Args: config(str): Running configuration Returns: dict: key: ospf_process_id (int) """ match = re.search(r'^router ospf (\d+)', config) return dict(ospf_process_id=int(match.group(1))) def _parse_vrf(self, config): """Parses config file for the OSPF vrf name Args: config(str): Running configuration Returns: dict: key: ospf_vrf (str) """ match = re.search(r'^router ospf \d+ vrf (\w+)', config) if match: return dict(vrf=match.group(1)) return dict(vrf='default') def _parse_router_id(self, config): """Parses config file for the OSPF router ID Args: config(str): Running configuration Returns: dict: key: router_id (str) """ match = re.search(r'router-id ([^\s]+)', config) value = match.group(1) if match else None return dict(router_id=value) def _parse_networks(self, config): """Parses config file for the networks advertised by the OSPF process Args: config(str): Running configuration Returns: list: dict: keys: network (str) netmask (str) area (str) """ networks = list() regexp = r'network (.+)/(\d+) area (\d+\.\d+\.\d+\.\d+)' matches = re.findall(regexp, config) for (network, netmask, area) in matches: networks.append(dict(network=network, netmask=netmask, area=area)) return dict(networks=networks) def _parse_redistribution(self, config): """Parses config file for the OSPF router ID Args: config (str): Running configuration Returns: list: dict: keys: protocol (str) route-map (optional) (str) """ redistributions = list() regexp = r'redistribute .*' matches = re.findall(regexp, config) for line in matches: ospf_redist = line.split() if len(ospf_redist) == 2: # simple redist: eg 'redistribute bgp' protocol = ospf_redist[1] redistributions.append(dict(protocol=protocol)) if len(ospf_redist) == 4: # complex redist eg 'redistribute bgp route-map NYSE-RP-MAP' protocol = ospf_redist[1] route_map_name = ospf_redist[3] redistributions.append(dict(protocol=protocol, route_map=route_map_name)) return dict(redistributions=redistributions) def _parse_shutdown(self, config): """Parses config file for the OSPF router ID Args: config(str): Running configuration Returns: dict: key: shutdown (bool) """ value = 'no shutdown' in config return dict(shutdown=not value) def set_shutdown(self): """Shutdowns the OSPF process Args: None Returns: bool: True if the commands are completed successfully """ cmd = 'shutdown' return self.configure_ospf(cmd) def set_no_shutdown(self): """Removes the shutdown property from the OSPF process Args: None Returns: bool: True if the commands are completed successfully """ cmd = 'no shutdown' return self.configure_ospf(cmd) def delete(self): """Removes the entire ospf process from the running configuration Args: None Returns: bool: True if the command completed succssfully """ config = self.get() if not config: return True command = 'no router ospf {}'.format(config['ospf_process_id']) return self.configure(command) def create(self, ospf_process_id, vrf=None): """Creates a OSPF process in the specified VRF or the default VRF. Args: ospf_process_id (str): The OSPF process Id value vrf (str): The VRF to apply this OSPF process to Returns: bool: True if the command completed successfully Exception: ValueError: If the ospf_process_id passed in less than 0 or greater than 65536 """ value = int(ospf_process_id) if not 0 < value < 65536: raise ValueError('ospf as must be between 1 and 65535') command = 'router ospf {}'.format(ospf_process_id) if vrf: command += ' vrf %s' % vrf return self.configure(command) def configure_ospf(self, cmd): """Allows for a list of OSPF subcommands to be configured" Args: cmd: (list or str): Subcommand to be entered Returns: bool: True if all the commands completed successfully """ config = self.get() cmds = ['router ospf {}'.format(config['ospf_process_id'])] cmds.extend(make_iterable(cmd)) return super(Ospf, self).configure(cmds) def set_router_id(self, value=None, default=False, disable=False): """Controls the router id property for the OSPF Proccess Args: value (str): The router-id value default (bool): Controls the use of the default keyword disable (bool): Controls the use of the no keyword Returns: bool: True if the commands are completed successfully """ cmd = self.command_builder('router-id', value=value, default=default, disable=disable) return self.configure_ospf(cmd) def add_network(self, network, netmask, area=0): """Adds a network to be advertised by OSPF Args: network (str): The network to be advertised in dotted decimal notation netmask (str): The netmask to configure area (str): The area the network belongs to. By default this value is 0 Returns: bool: True if the command completes successfully Exception: ValueError: This will get raised if network or netmask are not passed to the method """ if network == '' or netmask == '': raise ValueError('network and mask values ' 'may not be empty') cmd = 'network {}/{} area {}'.format(network, netmask, area) return self.configure_ospf(cmd) def remove_network(self, network, netmask, area=0): """Removes a network advertisment by OSPF Args: network (str): The network to be removed in dotted decimal notation netmask (str): The netmask to configure area (str): The area the network belongs to. By default this value is 0 Returns: bool: True if the command completes successfully Exception: ValueError: This will get raised if network or netmask are not passed to the method """ if network == '' or netmask == '': raise ValueError('network and mask values ' 'may not be empty') cmd = 'no network {}/{} area {}'.format(network, netmask, area) return self.configure_ospf(cmd) def add_redistribution(self, protocol, route_map_name=None): """Adds a protocol redistribution to OSPF Args: protocol (str): protocol to redistribute route_map_name (str): route-map to be used to filter the protocols Returns: bool: True if the command completes successfully Exception: ValueError: This will be raised if the protocol pass is not one of the following: [rip, bgp, static, connected] """ protocols = ['bgp', 'rip', 'static', 'connected'] if protocol not in protocols: raise ValueError('redistributed protocol must be' 'bgp, connected, rip or static') if route_map_name is None: cmd = 'redistribute {}'.format(protocol) else: cmd = 'redistribute {} route-map {}'.format(protocol, route_map_name) return self.configure_ospf(cmd) def remove_redistribution(self, protocol): """Removes a protocol redistribution to OSPF Args: protocol (str): protocol to redistribute route_map_name (str): route-map to be used to filter the protocols Returns: bool: True if the command completes successfully Exception: ValueError: This will be raised if the protocol pass is not one of the following: [rip, bgp, static, connected] """ protocols = ['bgp', 'rip', 'static', 'connected'] if protocol not in protocols: raise ValueError('redistributed protocol must be' 'bgp, connected, rip or static') cmd = 'no redistribute {}'.format(protocol) return self.configure_ospf(cmd) def instance(api): """Returns an instance of Ospf """ return Ospf(api) pyeapi-1.0.2/pyeapi/api/routemaps.py0000644000076500000240000003437714447405743020622 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with EOS routemap resources The Routemap resource provides configuration management of global route-map resources on an EOS node. It provides the following class implementations: * Routemaps - Configures routemaps in EOS Notes: The set and match attributes produce a list of strings with The corresponding configuration. These strings will omit the preceeding set or match words, respectively. """ import re from pyeapi.api import EntityCollection class Routemaps(EntityCollection): """The Routemaps class provides management of the routemaps configuration The Routemaps class is derived from Entity and provides an API for working with the nodes routemaps configuraiton. """ def get(self, name): """Provides a method to retrieve all routemap configuration related to the name attribute. Args: name (string): The name of the routemap. Returns: None if the specified routemap does not exists. If the routermap exists a dictionary will be provided as follows:: { 'deny': { 30: { 'continue': 200, 'description': None, 'match': ['as 2000', 'source-protocol ospf', 'interface Ethernet2'], 'set': [] } }, 'permit': { 10: { 'continue': 100, 'description': None, 'match': ['interface Ethernet1'], 'set': ['tag 50']}, 20: { 'continue': 200, 'description': None, 'match': ['as 2000', 'source-protocol ospf', 'interface Ethernet2'], 'set': [] } } } """ if not self.get_block(r'route-map\s%s\s\w+\s\d+' % name): return None return self._parse_entries(name) def getall(self): resources = dict() routemaps_re = re.compile(r'^route-map\s([\w-]+)\s\w+\s\d+$', re.M) for name in routemaps_re.findall(self.config): routemap = self.get(name) if routemap: resources[name] = routemap return resources def _parse_entries(self, name): routemap_re = re.compile(r'^route-map\s%s\s(\w+)\s(\d+)$' % name, re.M) entries = list() for entry in routemap_re.findall(self.config): resource = dict() action, seqno = entry routemap = self.get_block(r'route-map\s%s\s%s\s%s' % (name, action, seqno)) resource = dict(name=name, action=action, seqno=seqno, attr=dict()) resource['attr'].update(self._parse_match_statements(routemap)) resource['attr'].update(self._parse_set_statements(routemap)) resource['attr'].update(self._parse_continue_statement(routemap)) resource['attr'].update(self._parse_description(routemap)) entries.append(resource) return self._merge_entries(entries) def _merge_entries(self, entries): response = dict() for e in entries: action = e['action'] seqno = int(e['seqno']) if not response.get(action): response[action] = dict() response[action][seqno] = e['attr'] return response def _parse_match_statements(self, config): match_re = re.compile(r'^\s+match\s(.+)$', re.M) return dict(match=match_re.findall(config)) def _parse_set_statements(self, config): set_re = re.compile(r'^\s+set\s(.+)$', re.M) return dict(set=set_re.findall(config)) def _parse_continue_statement(self, config): continue_re = re.compile(r'^\s+continue\s(\d+)$', re.M) match = continue_re.search(config) value = int(match.group(1)) if match else None return {'continue': value} def _parse_description(self, config): desc_re = re.compile(r'^\s+description\s(.+)$', re.M) match = desc_re.search(config) value = match.group(1) if match else None return dict(description=value) def create(self, name, action, seqno): """Creates a new routemap on the node Note: This method will attempt to create the routemap regardless if the routemap exists or not. If the routemap already exists then this method will still return True. Args: name (string): The full name of the routemap. action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. Returns: True if the routemap could be created otherwise False (see Note) """ return self.configure('route-map %s %s %s' % (name, action, seqno)) def delete(self, name, action, seqno): """Deletes the routemap from the node Note: This method will attempt to delete the routemap from the nodes operational config. If the routemap does not exist then this method will not perform any changes but still return True Args: name (string): The full name of the routemap. action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. Returns: True if the routemap could be deleted otherwise False (see Node) """ return self.configure('no route-map %s %s %s' % (name, action, seqno)) def default(self, name, action, seqno): """Defaults the routemap on the node Note: This method will attempt to default the routemap from the nodes operational config. Since routemaps do not exist by default, the default action is essentially a negation and the result will be the removal of the routemap clause. If the routemap does not exist then this method will not perform any changes but still return True Args: name (string): The full name of the routemap. action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. Returns: True if the routemap could be deleted otherwise False (see Node) """ return self.configure('default route-map %s %s %s' % (name, action, seqno)) def set_match_statements(self, name, action, seqno, statements): """Configures the match statements within the routemap clause. The final configuration of match statements will reflect the list of statements passed into the statements attribute. This implies match statements found in the routemap that are not specified in the statements attribute will be removed. Args: name (string): The full name of the routemap. action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. statements (list): A list of the match-related statements. Note that the statements should omit the leading match. Returns: True if the operation succeeds otherwise False """ try: current_statements = self.get(name)[action][seqno]['match'] except Exception: current_statements = [] commands = list() # remove set statements from current routemap for entry in set(current_statements).difference(statements): commands.append('route-map %s %s %s' % (name, action, seqno)) commands.append('no match %s' % entry) # add new set statements to the routemap for entry in set(statements).difference(current_statements): commands.append('route-map %s %s %s' % (name, action, seqno)) commands.append('match %s' % entry) return self.configure(commands) if commands else True def set_set_statements(self, name, action, seqno, statements): """Configures the set statements within the routemap clause. The final configuration of set statements will reflect the list of statements passed into the statements attribute. This implies set statements found in the routemap that are not specified in the statements attribute will be removed. Args: name (string): The full name of the routemap. action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. statements (list): A list of the set-related statements. Note that the statements should omit the leading set. Returns: True if the operation succeeds otherwise False """ try: current_statements = self.get(name)[action][seqno]['set'] except Exception: current_statements = [] commands = list() # remove set statements from current routemap for entry in set(current_statements).difference(statements): commands.append('route-map %s %s %s' % (name, action, seqno)) commands.append('no set %s' % entry) # add new set statements to the routemap for entry in set(statements).difference(current_statements): commands.append('route-map %s %s %s' % (name, action, seqno)) commands.append('set %s' % entry) return self.configure(commands) if commands else True def set_continue(self, name, action, seqno, value=None, default=False, disable=False): """Configures the routemap continue value Args: name (string): The full name of the routemap. action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. value (integer): The value to configure for the routemap continue default (bool): Specifies to default the routemap continue value disable (bool): Specifies to negate the routemap continue value Returns: True if the operation succeeds otherwise False is returned """ commands = ['route-map %s %s %s' % (name, action, seqno)] if default: commands.append('default continue') elif disable: commands.append('no continue') else: if not str(value).isdigit() or value < 1: raise ValueError('seqno must be a positive integer unless ' 'default or disable is specified') commands.append('continue %s' % value) return self.configure(commands) def set_description(self, name, action, seqno, value=None, default=False, disable=False): """Configures the routemap description Args: name (string): The full name of the routemap. action (string): The action to take for this routemap clause. seqno (integer): The sequence number for the routemap clause. value (string): The value to configure for the routemap description default (bool): Specifies to default the routemap description value disable (bool): Specifies to negate the routemap description Returns: True if the operation succeeds otherwise False is returned """ commands = ['route-map %s %s %s' % (name, action, seqno)] if value is not None: # Before assigning a new description, clear any existing desc commands.append(self.command_builder('description', disable=True)) commands.append(self.command_builder('description', value=value, default=default, disable=disable)) return self.configure(commands) def instance(node): """Returns an instance of Routemaps Args: node (Node): The node argument passes an instance of Node to the resource Returns: object: An instance of Routemaps """ return Routemaps(node) pyeapi-1.0.2/pyeapi/api/staticroute.py0000644000076500000240000003370314447405743021141 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with EOS static routes The staticroute resource provides configuration management of static route resources on an EOS node. It provides the following class implementations: * StaticRoute - Configure static routes in EOS StaticRoute Attributes: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address next_hop_ip (string): The next hop address on destination interface distance (int): Administrative distance for this route tag (int): Route tag route_name (string): Route name Notes: The 'default' prefix function of the 'ip route' command, 'default ip route ...', currently equivalent to the 'no ip route ...' command. """ import re from pyeapi.api import EntityCollection # Define the regex to match ip route lines (by lines in regex): # 'ip route' header # ip_dest # next_hop # next_hop_ip # distance # tag # name ROUTES_RE = re.compile(r'(?<=^ip route)' r' (\d+\.\d+\.\d+\.\d+\/\d+)' r' (\d+\.\d+\.\d+\.\d+|\S+)' r'(?: (\d+\.\d+\.\d+\.\d+))?' r' (\d+)' r'(?: tag (\d+))?' r'(?: name (\S+))?', re.M) class StaticRoute(EntityCollection): """The StaticRoute class provides a configuration instance for working with static routes """ def __str__(self): return 'StaticRoute' def get(self, name): """Retrieves the ip route information for the destination ip address specified. Args: name (string): The ip address of the destination in the form of A.B.C.D/E Returns: dict: An dict object of static route entries in the form:: { ip_dest: { next_hop: { next_hop_ip: { distance: { 'tag': tag, 'route_name': route_name } } } } } If the ip address specified does not have any associated static routes, then None is returned. Notes: The keys ip_dest, next_hop, next_hop_ip, and distance in the returned dictionary are the values of those components of the ip route specification. If a route does not contain a next_hop_ip, then that key value will be set as 'None'. """ # Return the route configurations for the specified ip address, # or None if its not found return self.getall().get(name) def getall(self): """Return all ip routes configured on the switch as a resource dict Returns: dict: An dict object of static route entries in the form:: { ip_dest: { next_hop: { next_hop_ip: { distance: { 'tag': tag, 'route_name': route_name } } } } } If the ip address specified does not have any associated static routes, then None is returned. Notes: The keys ip_dest, next_hop, next_hop_ip, and distance in the returned dictionary are the values of those components of the ip route specification. If a route does not contain a next_hop_ip, then that key value will be set as 'None'. """ # Find all the ip routes in the config matches = ROUTES_RE.findall(self.config) # Parse the routes and add them to the routes dict routes = dict() for match in matches: # Get the four identifying components ip_dest = match[0] next_hop = match[1] next_hop_ip = None if match[2] == '' else match[2] distance = int(match[3]) # Create the data dict with the remaining components data = {} data['tag'] = None if match[4] == '' else int(match[4]) data['route_name'] = None if match[5] == '' else match[5] # Build the complete dict entry from the four components # and the data. # temp_dict = parent_dict[key] = parent_dict.get(key, {}) # This creates the keyed dict in the parent_dict if it doesn't # exist, or reuses the existing keyed dict. # The temp_dict is used to make things more readable. ip_dict = routes[ip_dest] = routes.get(ip_dest, {}) nh_dict = ip_dict[next_hop] = ip_dict.get(next_hop, {}) nhip_dict = nh_dict[next_hop_ip] = nh_dict.get(next_hop_ip, {}) nhip_dict[distance] = data return routes def create(self, ip_dest, next_hop, **kwargs): """Create a static route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address **kwargs['next_hop_ip'] (string): The next hop address on destination interface **kwargs['distance'] (string): Administrative distance for this route **kwargs['tag'] (string): Route tag **kwargs['route_name'] (string): Route name Returns: True if the operation succeeds, otherwise False. """ # Call _set_route with delete and default set to False return self._set_route(ip_dest, next_hop, **kwargs) def delete(self, ip_dest, next_hop, **kwargs): """Delete a static route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address **kwargs['next_hop_ip'] (string): The next hop address on destination interface **kwargs['distance'] (string): Administrative distance for this route **kwargs['tag'] (string): Route tag **kwargs['route_name'] (string): Route name Returns: True if the operation succeeds, otherwise False. """ # Call _set_route with the delete flag set to True kwargs.update({'delete': True}) return self._set_route(ip_dest, next_hop, **kwargs) def default(self, ip_dest, next_hop, **kwargs): """Set a static route to default (i.e. delete the matching route) Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address **kwargs['next_hop_ip'] (string): The next hop address on destination interface **kwargs['distance'] (string): Administrative distance for this route **kwargs['tag'] (string): Route tag **kwargs['route_name'] (string): Route name Returns: True if the operation succeeds, otherwise False. """ # Call _set_route with the default flag set to True kwargs.update({'default': True}) return self._set_route(ip_dest, next_hop, **kwargs) def set_tag(self, ip_dest, next_hop, **kwargs): """Set the tag value for the specified route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address **kwargs['next_hop_ip'] (string): The next hop address on destination interface **kwargs['distance'] (string): Administrative distance for this route **kwargs['tag'] (string): Route tag **kwargs['route_name'] (string): Route name Returns: True if the operation succeeds, otherwise False. Notes: Any existing route_name value must be included in call to set_tag, otherwise the tag will be reset by the call to EOS. """ # Call _set_route with the new tag information return self._set_route(ip_dest, next_hop, **kwargs) def set_route_name(self, ip_dest, next_hop, **kwargs): """Set the route_name value for the specified route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address **kwargs['next_hop_ip'] (string): The next hop address on destination interface **kwargs['distance'] (string): Administrative distance for this route **kwargs['tag'] (string): Route tag **kwargs['route_name'] (string): Route name Returns: True if the operation succeeds, otherwise False. Notes: Any existing tag value must be included in call to set_route_name, otherwise the tag will be reset by the call to EOS. """ # Call _set_route with the new route_name information return self._set_route(ip_dest, next_hop, **kwargs) def _build_commands(self, ip_dest, next_hop, **kwargs): """Build the EOS command string for ip route interactions. Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address **kwargs['next_hop_ip'] (string): The next hop address on destination interface **kwargs['distance'] (string): Administrative distance for this route **kwargs['tag'] (string): Route tag **kwargs['route_name'] (string): Route name Returns the ip route command string to be sent to the switch for the given set of parameters. """ commands = "ip route %s %s" % (ip_dest, next_hop) next_hop_ip = kwargs.get('next_hop_ip', None) distance = kwargs.get('distance', None) tag = kwargs.get('tag', None) route_name = kwargs.get('route_name', None) if next_hop_ip is not None: commands += " %s" % next_hop_ip if distance is not None: commands += " %s" % distance if tag is not None: commands += " tag %s" % tag if route_name is not None: commands += " name %s" % route_name return commands def _set_route(self, ip_dest, next_hop, **kwargs): """Configure a static route Args: ip_dest (string): The ip address of the destination in the form of A.B.C.D/E next_hop (string): The next hop interface or ip address **kwargs['next_hop_ip'] (string): The next hop address on destination interface **kwargs['distance'] (string): Administrative distance for this route **kwargs['tag'] (string): Route tag **kwargs['route_name'] (string): Route name **kwargs['delete'] (boolean): If true, deletes the specified route instead of creating or setting values for the route **kwargs['default'] (boolean): If true, defaults the specified route instead of creating or setting values for the route Returns: True if the operation succeeds, otherwise False. """ commands = self._build_commands(ip_dest, next_hop, **kwargs) delete = kwargs.get('delete', False) default = kwargs.get('default', False) # Prefix with 'no' if delete is set if delete: commands = "no " + commands # Or with 'default' if default is setting else: if default: commands = "default " + commands return self.configure(commands) def instance(node): """Returns an instance of StaticRoute This method will create and return an instance of the StaticRoute object passing the value of API to the object. The instance method is required for the resource to be autoloaded by the Node object Args: node (Node): The node argument passes an instance of Node to the resource """ return StaticRoute(node) pyeapi-1.0.2/pyeapi/api/stp.py0000644000076500000240000003234514447405743017402 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with spanning-tree in EOS This module provides an API for working with spanning-tree configuration in EOS. This includes both global spanning-tree configuration as well as interface config. Global Parameters: mode (string): The spanning-tree operational mode. Accepted values for this version are 'mstp' or 'none'. This configuration parameter is not defaultable interfaces (StpInterfaces): The collection of STP enabled interfaces. Interface Parameters: name (string): The name of the interface the STP configuration is in reference to. The interface name is the full interface identifier portfast (string): The portfast configuration value for the interface. Accepted values are 'edge', 'network', or 'disabled' bpduguard (boolean): True if the BPDU Guard feature is enabled on the interface or False if it is disabled """ import re from pyeapi.api import Entity, EntityCollection class Stp(Entity): """The Stp class implements global configuration for spanning-tree The spanning-tree protocol provides both global and interface configuration options. This class is the top-level class that provides access to all spanning-tree configuration options supported. Example: The below example demonstrates how to use the STP class to work with both global configuration and interface configuration. >>> import pyeapi.resources.stp >>> stp = pyeapi.resources.stp.instance(node) >>> stp.set_mode('mstp') True >>> stp.interfaces.set_bpduguard('Ethernet1', True) True Attributes: interfaces (StpInterfaces): An instance for configuration spanning-tree configuration interfaces instances (StpInstances): An instance object for working with spanning-tree global instances """ def __init__(self, *args, **kwargs): super(Stp, self).__init__(*args, **kwargs) self._interfaces = None self._instances = None def get(self): """Returns the spanning-tree configuration as a dict object The dictionary object represents the entire spanning-tree configuration derived from the nodes running config. This includes both globally configuration attributes as well as interfaces and instances. See the StpInterfaces and StpInstances classes for the key/value pair definitions. Note: See the individual classes for detailed message structures Returns: A Python dictionary object of key/value pairs the represent the entire supported spanning-tree configuration:: { "mode": [mstp, none], "interfaces": {...}, "instances": {...} } """ return dict(interfaces=self.interfaces.getall(), instances=self.instances.getall()) @property def interfaces(self): if self._interfaces is not None: return self._interfaces self._interfaces = StpInterfaces(self.node) return self._interfaces @property def instances(self): if self._instances is not None: return self._instances self._instances = StpInstances(self.node) return self._instances def set_mode(self, value=None, default=False, disable=False): """Configures the global spanning-tree mode Note: This configuration parameter is not defaultable Args: value (string): The value to configure the global spanning-tree mode of operation. Valid values include 'mstp', 'none' default (bool): Set the global spanning-tree mode to default. disable (bool): Negate the global spanning-tree mode. Returns: True if the configuration operation succeeds otherwise False Raises: ValueError if the value is not in the accepted range """ if not default and not disable: if value not in ['mstp', 'none']: raise ValueError("Specified value must be one of " "'mstp', 'none'") cmds = self.command_builder('spanning-tree mode', value=value, default=default, disable=disable) return self.configure(cmds) class StpInstances(EntityCollection): """Provides a configuration resource for spanning-tree instances This class provides an API for working with spanning-tree instances from the global configuration. Spanning tree instances work with MST configuration """ def getall(self): # TODO: (privateip, 20150106) stubbed out, needs implementation return dict() class StpInterfaces(EntityCollection): """Provides a configuration resource for spanning-tree interfaces This class provides an API for working with spanning-tree interface configurations. It provides access to managing specific interface spanning-tree configuration options. Note that spanning-tree interfaces cannot be created or deleted. """ def get(self, name): """Returns the specified interfaces STP configuration resource The STP interface resource contains the following * name (str): The interface name * portfast (bool): The spanning-tree portfast admin state * bpduguard (bool): The spanning-tree bpduguard admin state * portfast_type (str): The spanning-tree portfast value. Valid values include "edge", "network", "normal" Args: name (string): The interface identifier to retrieve the config for. Note: Spanning-tree interfaces are only supported on Ethernet and Port-Channel interfaces Returns: dict: A resource dict object that represents the interface configuration. None: If the specified interace is not a STP port """ if not isvalidinterface(name): return None config = self.get_block(r'^interface\s%s$' % name) resp = dict() resp.update(self._parse_bpduguard(config)) resp.update(self._parse_portfast(config)) resp.update(self._parse_portfast_type(config)) return resp def _parse_bpduguard(self, config): value = 'spanning-tree bpduguard enable' in config return dict(bpduguard=value) def _parse_portfast(self, config): value = 'no spanning-tree portfast' not in config return dict(portfast=value) def _parse_portfast_type(self, config): if 'spanning-tree portfast network' in config: value = 'network' elif 'no spanning-tree portfast' in config: value = 'normal' else: value = 'edge' return dict(portfast_type=value) def getall(self): """Returns the collection of STP interfaces This method will return all of the configured spanning-tree interfaces from the current nodes configuration. Returns: A Python dictionary object that represents all configured spanning-tree interfaces indexed by interface name. """ interfaces_re = re.compile(r'(?<=^interface\s)(.+)$', re.M) response = dict() for name in interfaces_re.findall(self.config): if name[0:2] in ['Et', 'Po']: interface = self.get(name) if interface: response[name] = interface return response def configure_interface(self, name, cmds): if not isvalidinterface(name): raise ValueError('invalid interface value specified') return super(StpInterfaces, self).configure_interface(name, cmds) def set_portfast_type(self, name, value='normal'): """Configures the portfast value for the specified interface Args: name (string): The interface identifier to configure. The name must be the full interface name (eg Ethernet1, not Et1). value (string): The value to configure the portfast setting to. Valid values include "edge", "network", "normal". The default value is "normal" Returns: True if the command succeeds, otherwise False Raises: ValueError: Raised if an invalid interface name or value is specified """ if value not in ['network', 'edge', 'normal', None]: raise ValueError('invalid portfast type value specified') cmds = ['spanning-tree portfast %s' % value] if value == 'edge': cmds.append('spanning-tree portfast auto') return self.configure_interface(name, cmds) def set_portfast(self, name, value=None, default=False, disable=False): """Configures the portfast value for the specified interface Args: name (string): The interface identifier to configure. The name must be the full interface name (eg Ethernet1, not Et1) value (bool): True if portfast is enabled otherwise False default (bool): Configures the portfast parameter to its default value using the EOS CLI default config command disable (bool): Negates the portfast parameter using the EOS CLI no config command Returns: True if the command succeeds, otherwise False Raises: ValueError: Rasied if an invalid interface name is specified TypeError: Raised if the value keyword argument does not evaluate to a valid boolean """ if value is False: disable = True string = 'spanning-tree portfast' cmds = self.command_builder(string, value=value, default=default, disable=disable) return self.configure_interface(name, cmds) def set_bpduguard(self, name, value=False, default=False, disable=False): """Configures the bpduguard value for the specified interface Args: name (string): The interface identifier to configure. The name must be the full interface name (eg Ethernet1, not Et1) value (bool): True if bpduguard is enabled otherwise False default (bool): Configures the bpduguard parameter to its default value using the EOS CLI default config command disable (bool): Negates the bpduguard parameter using the EOS CLI no config command Returns: True if the command succeeds, otherwise False Raises: ValueError: Rasied if an invalid interface name is specified TypeError: Raised if the value keyword argument does not evaluate to a valid boolean """ value = 'enable' if value else 'disable' string = 'spanning-tree bpduguard' cmds = self.command_builder(string, value=value, default=default, disable=disable) return self.configure_interface(name, cmds) def isvalidinterface(value): """Checks value to see if it could be a spanning-tree interface This function will check the value and return a boolean whether or not the interface could be a spanning-tree interface Note: This function only checks *if* the interface could be a spanning-tree interface but does not check if it *is* configured as a spanning-tree interface Args: value (string): The interface name to validate Returns: True if it could be a spanning-tree interface, otherwise False """ return value[0:2] in ['Et', 'Po'] def instance(api): return Stp(api) pyeapi-1.0.2/pyeapi/api/switchports.py0000644000076500000240000004047314447405743021166 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with logical layer 2 switchports in EOS This module provides an API for working with logical layer 2 interfaces (switchports) in EOS. Switchports are interfaces built on top of physical Ethernet and bundled Port-Channel interfaces. """ import re from pyeapi.api import EntityCollection from pyeapi.utils import make_iterable class Switchports(EntityCollection): """The Switchports class provides a configuration resource for swichports Logical layer 2 interfaces built on top of physical Ethernet and bundled Port-Channel interfaces can be configured and managed with an instance of Switchports. The Switchports class is a resource collection and supports get and getall methods. The Switchports class is derived from the BaseResource class """ def get(self, name): """Returns a dictionary object that represents a switchport The Switchport resource returns the following: * name (str): The name of the interface * mode (str): The switchport mode value * access_vlan (str): The switchport access vlan value * trunk_native_vlan (str): The switchport trunk native vlan vlaue * trunk_allowed_vlans (str): The trunk allowed vlans value * trunk_groups (list): The list of trunk groups configured Args: name (string): The interface identifier to get. Note: Switchports are only supported on Ethernet and Port-Channel interfaces Returns: dict: A Python dictionary object of key/value pairs that represent the switchport configuration for the interface specified If the specified argument is not a switchport then None is returned """ config = self.get_block('interface %s' % name) if 'no switchport\n' in config: return resource = dict(name=name) resource.update(self._parse_mode(config)) resource.update(self._parse_access_vlan(config)) resource.update(self._parse_trunk_native_vlan(config)) resource.update(self._parse_trunk_allowed_vlans(config)) resource.update(self._parse_trunk_groups(config)) return resource def _parse_mode(self, config): """Scans the specified config and parses the switchport mode value Args: config (str): The interface configuration block to scan Returns: dict: A Python dict object with the value of switchport mode. The dict returned is intended to be merged into the resource dict """ value = re.search(r'switchport mode (\w+)', config, re.M) return dict(mode=value.group(1)) def _parse_trunk_groups(self, config): """Scans the specified config and parses the trunk group values Args: config (str): The interface configuraiton blcok Returns: A dict object with the trunk group values that can be merged into the resource dict """ values = re.findall(r'switchport trunk group ([^\s]+)', config, re.M) return dict(trunk_groups=values) def _parse_access_vlan(self, config): """Scans the specified config and parse the access-vlan value Args: config (str): The interface configuration block to scan Returns: dict: A Python dict object with the value of switchport access value. The dict returned is intended to be merged into the resource dict """ value = re.search(r'switchport access vlan (\d+)', config) return dict(access_vlan=value.group(1) if value else None) def _parse_trunk_native_vlan(self, config): """Scans the specified config and parse the trunk native vlan value Args: config (str): The interface configuration block to scan Returns: dict: A Python dict object with the value of switchport trunk native vlan value. The dict returned is intended to be merged into the resource dict """ match = re.search(r'switchport trunk native vlan (\d+)', config) return dict(trunk_native_vlan=match.group(1)) def _parse_trunk_allowed_vlans(self, config): """Scans the specified config and parse the trunk allowed vlans value Args: config (str): The interface configuration block to scan Returns: dict: A Python dict object with the value of switchport trunk allowed vlans value. The dict returned is intended to be merged into the resource dict """ match = re.search(r'switchport trunk allowed vlan (.+)$', config, re.M) return dict(trunk_allowed_vlans=match.group(1)) def getall(self): """Returns a dict object to all Switchports This method will return all of the configured switchports as a dictionary object keyed by the interface identifier. Returns: A Python dictionary object that represents all configured switchports in the current running configuration """ interfaces_re = re.compile(r'(?<=^interface\s)([Et|Po][^.\s]+)$', re.M) response = dict() for name in interfaces_re.findall(self.config): interface = self.get(name) if interface: response[name] = interface return response def create(self, name): """Creates a new logical layer 2 interface This method will create a new switchport for the interface specified in the arguments (name). If the logical switchport already exists then this command will have no effect Args: name (string): The interface identifier to create the logical layer 2 switchport for. The name must be the full interface name and not an abbreviated interface name (eg Ethernet1, not Et1) Returns: True if the create operation succeeds otherwise False. If the interface specified in args is already a switchport then this method will have no effect but will still return True """ commands = ['interface %s' % name, 'no ip address', 'switchport'] return self.configure(commands) def delete(self, name): """Deletes the logical layer 2 interface This method will delete the logical switchport for the interface specified in the arguments. If the interface doe not have a logical layer 2 interface defined, then this method will have no effect. Args: name (string): The interface identifier to create the logical layer 2 switchport for. The name must be the full interface name and not an abbreviated interface name (eg Ethernet1, not Et1) Returns: True if the create operation succeeds otherwise False. If the interface specified in args is already a switchport then this method will have no effect but will still return True """ commands = ['interface %s' % name, 'no switchport'] return self.configure(commands) def default(self, name): """Defaults the configuration of the switchport interface This method will default the configuration state of the logical layer 2 interface. Args: name (string): The interface identifier to create the logical layer 2 switchport for. The name must be the full interface name and not an abbreviated interface name (eg Ethernet1, not Et1) Returns: True if the create operation succeeds otherwise False. If the interface specified in args is already a switchport then this method will have no effect but will still return True """ commands = ['interface %s' % name, 'no ip address', 'default switchport'] return self.configure(commands) def set_mode(self, name, value=None, default=False, disable=False): """Configures the switchport mode Args: name (string): The interface identifier to create the logical layer 2 switchport for. The name must be the full interface name and not an abbreviated interface name (eg Ethernet1, not Et1) value (string): The value to set the mode to. Accepted values for this argument are access or trunk default (bool): Configures the mode parameter to its default value using the EOS CLI disable (bool): Negate the mode parameter using the EOS CLI Returns: True if the create operation succeeds otherwise False. """ string = 'switchport mode' command = self.command_builder(string, value=value, default=default, disable=disable) return self.configure_interface(name, command) def set_access_vlan(self, name, value=None, default=False, disable=False): """Configures the switchport access vlan Args: name (string): The interface identifier to create the logical layer 2 switchport for. The name must be the full interface name and not an abbreviated interface name (eg Ethernet1, not Et1) value (string): The value to set the access vlan to. The value must be a valid VLAN ID in the range of 1 to 4094. default (bool): Configures the access vlan parameter to its default value using the EOS CLI disable (bool): Negate the access vlan parameter using the EOS CLI Returns: True if the create operation succeeds otherwise False. """ string = 'switchport access vlan' command = self.command_builder(string, value=value, default=default, disable=disable) return self.configure_interface(name, command) def set_trunk_native_vlan(self, name, value=None, default=False, disable=False): """Configures the switchport trunk native vlan value Args: name (string): The interface identifier to create the logical layer 2 switchport for. The name must be the full interface name and not an abbreviated interface name (eg Ethernet1, not Et1) value (string): The value to set the trunk nativevlan to. The value must be a valid VLAN ID in the range of 1 to 4094. default (bool): Configures the access vlan parameter to its default value using the EOS CLI disable (bool): Negate the access vlan parameter using the EOS CLI Returns: True if the create operation succeeds otherwise False. """ string = 'switchport trunk native vlan' command = self.command_builder(string, value=value, default=default, disable=disable) return self.configure_interface(name, command) def set_trunk_allowed_vlans(self, name, value=None, default=False, disable=False): """Configures the switchport trunk allowed vlans value Args: name (string): The interface identifier to create the logical layer 2 switchport for. The name must be the full interface name and not an abbreviated interface name (eg Ethernet1, not Et1) value (string): The value to set the trunk allowed vlans to. The value must be a valid VLAN ID in the range of 1 to 4094. default (bool): Configures the access vlan parameter to its default value using the EOS CLI disable (bool): Negate the access vlan parameter using the EOS CLI Returns: True if the create operation succeeds otherwise False. """ string = 'switchport trunk allowed vlan' command = self.command_builder(string, value=value, default=default, disable=disable) return self.configure_interface(name, command) def set_trunk_groups(self, intf, value=None, default=False, disable=False): """Configures the switchport trunk group value Args: intf (str): The interface identifier to configure. value (str): The set of values to configure the trunk group default (bool): Configures the trunk group default value disable (bool): Negates all trunk group settings Returns: True if the config operation succeeds otherwise False """ if default: cmd = 'default switchport trunk group' return self.configure_interface(intf, cmd) if disable: cmd = 'no switchport trunk group' return self.configure_interface(intf, cmd) current_value = self.get(intf)['trunk_groups'] failure = False value = make_iterable(value) for name in set(value).difference(current_value): if not self.add_trunk_group(intf, name): failure = True for name in set(current_value).difference(value): if not self.remove_trunk_group(intf, name): failure = True return not failure def add_trunk_group(self, intf, value): """Adds the specified trunk group to the interface Args: intf (str): The interface name to apply the trunk group to value (str): The trunk group value to apply to the interface Returns: True if the operation as successfully applied otherwise false """ string = 'switchport trunk group {}'.format(value) return self.configure_interface(intf, string) def remove_trunk_group(self, intf, value): """Removes a specified trunk group to the interface Args: intf (str): The interface name to remove the trunk group from value (str): The trunk group value Returns: True if the operation as successfully applied otherwise false """ string = 'no switchport trunk group {}'.format(value) return self.configure_interface(intf, string) def instance(node): """Returns an instance of Switchports This method will create and return an instance of the Switchports object passing the value of node to the instance. The module method is required for the resource to be autoloaded by the Node object Args: node (Node): The node argument provides an instance of Node to the resource """ return Switchports(node) pyeapi-1.0.2/pyeapi/api/system.py0000644000076500000240000001517514447405743020122 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with the global system in EOS This module provides an API for working with the global system settings in EOS. It provides the following class implementations: * System -- Configures global system settings System Attributes: hostname (string): The hostname of the node as configured in the running-configuration. """ import re from pyeapi.api import Entity class System(Entity): """The System class implements global config for the node Global configuration settings include those thaat identify the node and provide node level configuration such as hostname """ def get(self): """Returns the system configuration abstraction The System resource returns the following: * hostname (str): The hostname value Returns: dict: Represents the node's system configuration """ resource = dict() resource.update(self._parse_hostname()) resource.update(self._parse_iprouting()) resource.update(self._parse_banners()) return resource def _parse_hostname(self): """Parses the global config and returns the hostname value Returns: dict: The configured value for hostname. The returned dict object is intended to be merged into the resource dict """ value = 'localhost' match = re.search(r'^hostname ([^\s]+)$', self.config, re.M) if match: value = match.group(1) return dict(hostname=value) def _parse_iprouting(self): """Parses the global config and returns the ip routing value Returns: dict: The configure value for ip routing. The returned dict object is intendd to be merged into the resource dict """ value = 'no ip routing' not in self.config return dict(iprouting=value) def _parse_banners(self): """Parses the global config and returns the value for both motd and login banners. Returns: dict: The configure value for modtd and login banners. If the banner is not set it will return a value of None for that key. The returned dict object is intendd to be merged into the resource dict """ motd_value = login_value = None matches = re.findall(r'^banner\s+(login|motd)\s?$\n(.*?)$\nEOF$\n', self.config, re.DOTALL | re.M) for match in matches: if match[0].strip() == "motd": motd_value = match[1] elif match[0].strip() == "login": login_value = match[1] return dict(banner_motd=motd_value, banner_login=login_value) def set_hostname(self, value=None, default=False, disable=False): """Configures the global system hostname setting EosVersion: 4.13.7M Args: value (str): The hostname value default (bool): Controls use of the default keyword disable (bool): Controls the use of the no keyword Returns: bool: True if the commands are completed successfully """ cmd = self.command_builder('hostname', value=value, default=default, disable=disable) return self.configure(cmd) def set_iprouting(self, value=None, default=False, disable=False): """Configures the state of global ip routing EosVersion: 4.13.7M Args: value(bool): True if ip routing should be enabled or False if ip routing should be disabled default (bool): Controls the use of the default keyword disable (bool): Controls the use of the no keyword Returns: bool: True if the commands completed successfully otherwise False """ if value is False: disable = True cmd = self.command_builder('ip routing', value=value, default=default, disable=disable) return self.configure(cmd) def set_banner(self, banner_type, value=None, default=False, disable=False): """Configures system banners Args: banner_type(str): banner to be changed (likely login or motd) value(str): value to set for the banner default (bool): Controls the use of the default keyword disable (bool): Controls the use of the no keyword` Returns: bool: True if the commands completed successfully otherwise False """ command_string = "banner %s" % banner_type if default is True or disable is True: cmd = self.command_builder(command_string, value=None, default=default, disable=disable) return self.configure(cmd) else: if not value.endswith("\n"): value = value + "\n" command_input = dict(cmd=command_string, input=value) return self.configure([command_input]) def instance(api): """Returns an instance of System """ return System(api) pyeapi-1.0.2/pyeapi/api/users.py0000644000076500000240000002730414447405743017734 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """API Module for working with EOS local user resources The Users resource provides configuration of local user resources for an EOS node. Parameters: username (string): The username parameter maps to the local username defined in the running-config. nopassword (boolean): Configures the username to use no password at login. This parameter is mutually exclusive with secret privilege (integer): Configures the user privilege level in EOS role (string): Configures the users role in EOS secret (string): Configures the users secret (password) to use at login. This parameter is mutually exclusive with secret and is used in conjunction with format. format (string): Configures the format of the secret value. Accepted values for format are "cleartext", "md5", "nologin" and "sha512" """ import re from pyeapi.api import EntityCollection DEFAULT_ENCRYPTION = 'cleartext' ENCRYPTION_MAP = {'cleartext': 0, 'md5': 5, 'sha512': 'sha512', 'nologin': '*'} def isprivilege(value): """Checks value for valid privilege level Args: value (str, int): Checks if value is a valid user privilege Returns: True if the value is valid, otherwise False """ try: value = int(value) return 0 <= value < 16 except ValueError: return False class Users(EntityCollection): """The Users class provides a configuration resource for local users. The regex used here parses the running configuration to find username entries. There is extra logic in the regular expression to store the username as 'user' and then creates a backreference to find a following configuration line that might contain the users sshkey. """ def get(self, name): """Returns the local user configuration as a resource dict Args: name (str): The username to return from the nodes global running- config. Returns: dict: A resource dict object If the `name` does not exist, then None is returned """ return self.getall().get(name) def getall(self): """Returns all local users configuration as a resource dict Returns: dict: A dict of usernames with a nested resource dict object """ if self.version_number >= '4.23': self.users_re = re.compile(r'username (?P[^\s]+) ' r'privilege (\d+)' r'(?: role ([^\s]+))?' r'(?: (nopassword))?' r'(?: secret (0|5|7|sha512) (.+))?' r'.*$\n(?:username (?P=user) ' r'ssh.key (.+)$)?', re.M) else: self.users_re = re.compile(r'username (?P[^\s]+) ' r'privilege (\d+)' r'(?: role ([^\s]+))?' r'(?: (nopassword))?' r'(?: secret (0|5|7|sha512) (.+))?' r'.*$\n(?:username (?P=user) ' r'sshkey (.+)$)?', re.M) users = self.users_re.findall(self.config, re.M) resources = dict() for user in users: resources.update(self._parse_username(user)) return resources def _parse_username(self, config): """Scans the config block and returns the username as a dict Args: config (str): The config block to parse Returns: dict: A resource dict that is intended to be merged into the user resource """ (username, priv, role, nopass, fmt, secret, sshkey) = config resource = dict() resource['privilege'] = priv resource['role'] = role resource['nopassword'] = nopass == 'nopassword' resource['format'] = fmt resource['secret'] = secret if self.version_number >= '4.23': resource['ssh-key'] = sshkey else: resource['sshkey'] = sshkey return {username: resource} def create(self, name, nopassword=None, secret=None, encryption=None): """Creates a new user on the local system. Creating users requires either a secret (password) or the nopassword keyword to be specified. Args: name (str): The name of the user to craete nopassword (bool): Configures the user to be able to authenticate without a password challenage secret (str): The secret (password) to assign to this user encryption (str): Specifies how the secret is encoded. Valid values are "cleartext", "md5", "nologin", "sha512". The default is "cleartext" Returns: True if the operation was successful otherwise False Raises: TypeError: if the required arguments are not satisfied """ if secret is not None or encryption == 'nologin': return self.create_with_secret(name, secret, encryption) elif nopassword is True: return self.create_with_nopassword(name) else: raise TypeError('either "nopassword" or "secret" must be ' 'specified to create a user') def create_with_secret(self, name, secret, encryption): """Creates a new user on the local node Args: name (str): The name of the user to craete secret (str): The secret (password) to assign to this user encryption (str): Specifies how the secret is encoded. Valid values are "cleartext", "md5", "nologin" and "sha512". The default is "cleartext" Returns: True if the operation was successful otherwise False """ try: encryption = encryption or DEFAULT_ENCRYPTION enc = ENCRYPTION_MAP[encryption] except KeyError: raise TypeError('encryption must be one of "cleartext", "md5"' ' "nologin" or "sha512"') cmd = 'username %s secret %s %s' % (name, enc, secret) if encryption == 'nologin': cmd = 'username %s secret %s' % (name, enc) return self.configure(cmd) def create_with_nopassword(self, name): """Creates a new user on the local node Args: name (str): The name of the user to create Returns: True if the operation was successful otherwise False """ return self.configure('username %s nopassword' % name) def delete(self, name): """Deletes the local username from the config Args: name (str): The name of the user to delete Returns: True if the operation was successful otherwise False """ if name == 'admin': raise TypeError('the admin user cannot be deleted.') return self.configure('no username %s' % name) def default(self, name): """Configures the local username using the default keyword Args: name (str): The name of the user to configure Returns: True if the operation was successful otherwise False """ return self.configure('default username %s' % name) def set_privilege(self, name, value=None): """Configures the user privilege value in EOS Args: name (str): The name of the user to craete value (int): The privilege value to assign to the user. Valid values are in the range of 0 to 15 Returns: True if the operation was successful otherwise False Raises: TypeError: if the value is not in the valid range """ cmd = 'username %s' % name if value is not None: if not isprivilege(value): raise TypeError('priviledge value must be between 0 and 15') cmd += ' privilege %s' % value else: cmd += ' privilege 1' return self.configure(cmd) def set_role(self, name, value=None, default=False, disable=False): """Configures the user role vale in EOS Args: name (str): The name of the user to create value (str): The value to configure for the user role default (bool): Configure the user role using the EOS CLI default command disable (bool): Negate the user role using the EOS CLI no command Returns: True if the operation was successful otherwise False """ cmd = self.command_builder('username %s role' % name, value=value, default=default, disable=disable) return self.configure(cmd) def set_sshkey(self, name, value=None, default=False, disable=False): """Configures the user sshkey Args: name (str): The name of the user to add the sshkey to value (str): The value to configure for the sshkey. default (bool): Configure the sshkey using the EOS CLI default command disable (bool): Negate the sshkey using the EOS CLI no command Returns: True if the operation was successful otherwise False """ if self.version_number >= '4.23': cmd = self.command_builder('username %s ssh-key' % name, value=value, default=default, disable=disable) else: cmd = self.command_builder('username %s sshkey' % name, value=value, default=default, disable=disable) return self.configure(cmd) def instance(node): """Returns an instance of Users This method will create and return an instance of the Users object passing the value of API to the object. The instance method is required for the resource to be autoloaded by the Node object Args: node (Node): The node argument passes an instance of Node to the resource """ return Users(node) pyeapi-1.0.2/pyeapi/api/varp.py0000644000076500000240000002012114447405743017531 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for managing the VARP configuration in EOS This module provides an API for configuring VARP resources using EOS and eAPI. Arguments: name (string): The interface name the configuration is in reference to. The interface name is the full interface identifier address (string): The interface IP address in the form of address/len. mtu (integer): The interface MTU value. The MTU value accepts integers in the range of 68 to 65535 bytes """ import re from pyeapi.api import EntityCollection class Varp(EntityCollection): def __init__(self, *args, **kwargs): super(Varp, self).__init__(*args, **kwargs) self._interfaces = None @property def interfaces(self): if self._interfaces is not None: return self._interfaces self._interfaces = VarpInterfaces(self.node) return self._interfaces def get(self): """Returns the current VARP configuration The Varp resource returns the following: * mac_address (str): The virtual-router mac address * interfaces (dict): A list of the interfaces that have a virtual-router address configured. Return: A Python dictionary object of key/value pairs that represents the current configuration of the node. If the specified interface does not exist then None is returned:: { "mac_address": "aa:bb:cc:dd:ee:ff", "interfaces": { "Vlan100": { "addresses": [ "1.1.1.1", "2.2.2.2"] }, "Vlan200": [...] } } """ resource = dict() resource.update(self._parse_mac_address()) resource.update(self._parse_interfaces()) return resource def _parse_mac_address(self): mac_address_re = re.compile(r'^ip\svirtual-router\smac-address\s' r'((?:[a-f0-9]{2}:){5}[a-f0-9]{2})$', re.M) mac = mac_address_re.search(self.config) mac = mac.group(1) if mac else None return dict(mac_address=mac) def _parse_interfaces(self): interfaces = VarpInterfaces(self.node).getall() return dict(interfaces=interfaces) def set_mac_address(self, mac_address=None, default=False, disable=False): """ Sets the virtual-router mac address This method will set the switch virtual-router mac address. If a virtual-router mac address already exists it will be overwritten. Args: mac_address (string): The mac address that will be assigned as the virtual-router mac address. This should be in the format, aa:bb:cc:dd:ee:ff. default (bool): Sets the virtual-router mac address to the system default (which is to remove the configuration line). disable (bool): Negates the virtual-router mac address using the system no configuration command Returns: True if the set operation succeeds otherwise False. """ base_command = 'ip virtual-router mac-address' if not default and not disable: if mac_address is not None: # Check to see if mac_address matches expected format if not re.match(r'(?:[a-f0-9]{2}:){5}[a-f0-9]{2}', mac_address): raise ValueError('mac_address must be formatted like:' 'aa:bb:cc:dd:ee:ff') else: raise ValueError('mac_address must be a properly formatted ' 'address string') if default or disable and not mac_address: current_mac = self._parse_mac_address() if current_mac['mac_address']: base_command = base_command + ' ' + current_mac['mac_address'] commands = self.command_builder(base_command, value=mac_address, default=default, disable=disable) return self.configure(commands) class VarpInterfaces(EntityCollection): """The VarpInterfaces class helps manage interfaces with virtual-router configuration. """ def get(self, name): interface_re = r'interface\s%s' % name config = self.get_block(interface_re) if not config: return None resource = dict(addresses=dict()) resource.update(self._parse_virtual_addresses(config)) return resource def getall(self): resources = dict() interfaces_re = re.compile(r'^interface\s(Vlan\d+)$', re.M) for name in interfaces_re.findall(self.config): interface_detail = self.get(name) if interface_detail: resources[name] = interface_detail return resources def set_addresses(self, name, addresses=None, default=False, disable=False): commands = list() commands.append('interface %s' % name) if default: commands.append('default ip virtual-router address') elif disable: commands.append('no ip virtual-router address') elif addresses is not None: try: current_addresses = self.get(name)['addresses'] except Exception: current_addresses = [] # remove virtual-router addresses not present in addresses list for entry in set(current_addresses).difference(addresses): commands.append('no ip virtual-router address %s' % entry) # add new set virtual-router addresses that werent present for entry in set(addresses).difference(current_addresses): commands.append('ip virtual-router address %s' % entry) else: commands.append('no ip virtual-router address') return self.configure(commands) if commands else True def _parse_virtual_addresses(self, config): virt_ip_re = re.compile(r'^\s+ip\svirtual-router\saddress\s(\S+)$', re.M) return dict(addresses=virt_ip_re.findall(config)) def instance(node): """Returns an instance of Ipinterfaces This method will create and return an instance of the Varp object passing the value of node to the instance. This function is required for the resource to be autoloaded by the Node object Args: node (Node): The node argument provides an instance of Node to the Varp instance """ return Varp(node) pyeapi-1.0.2/pyeapi/api/vlans.py0000644000076500000240000003032414447405743017712 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with EOS VLAN resources The Vlans resource provides configuration of VLAN resources for an EOS node. Parameters: name (string): The name parameter maps to the VLAN name in EOS. Valid values include any consecutive sequence of numbers, letters and underscore up to the maximum number of characters. This parameter is defaultable. state (string): The state parameter sets the operational state of the VLAN on the node. It has two valid values: active or suspend. The state parameter is defaultable. trunk_groups (array): The trunk_groups parameter provides a list of trunk groups configured for this VLAN. This parameter is defaultable. """ import re from pyeapi.api import EntityCollection from pyeapi.utils import make_iterable VLAN_ID_RE = re.compile(r'(?:vlan\s)(?P.*)$', re.M) NAME_RE = re.compile(r'(?:name\s)(?P.*)$', re.M) STATE_RE = re.compile(r'(?:state\s)(?P.*)$', re.M) TRUNK_GROUP_RE = re.compile(r'(?:trunk\sgroup\s)(?P.*)$', re.M) def isvlan(value): """Checks if the argument is a valid VLAN A valid VLAN is an integer value in the range of 1 to 4094. This function will test if the argument falls into the specified range and is considered a valid VLAN Args: value: The value to check if is a valid VLAN Returns: True if the supplied value is a valid VLAN otherwise False """ try: value = int(value) return value in range(1, 4095) except ValueError: return False class Vlans(EntityCollection): """The Vlans class provides a configuration resource for VLANs The Vlans class is derived from ResourceBase a standard set of methods for working with VLAN configurations on an EOS node. """ def get(self, value): """Returns the VLAN configuration as a resource dict. Args: vid (string): The vlan identifier to retrieve from the running configuration. Valid values are in the range of 1 to 4095 Returns: A Python dict object containing the VLAN attributes as key/value pairs. """ config = self.get_block('vlan %s' % value) if not config: return None response = dict(vlan_id=self._parse_vlan_id(config)) response.update(self._parse_name(config)) response.update(self._parse_state(config)) response.update(self._parse_trunk_groups(config)) return response def _parse_vlan_id(self, config): """ _parse_vlan_id scans the provided configuration block and extracts the vlan id. The config block is expected to always return the vlan id. The return dict is intended to be merged into the response dict. Args: config (str): The vlan configuration block from the nodes running configuration Returns: Str: vlan id (or range/list of vlan ids) """ value = VLAN_ID_RE.search(config).group('value') return value def _parse_name(self, config): """ _parse_name scans the provided configuration block and extracts the vlan name. The config block is expected to always return the vlan name. The return dict is intended to be merged into the response dict. Args: config (str): The vlan configuration block from the nodes running configuration Returns: dict: resource dict attribute """ value = NAME_RE.search(config).group('value') return dict(name=value) def _parse_state(self, config): """ _parse_state scans the provided configuration block and extracts the vlan state value. The config block is expected to always return the vlan state config. The return dict is inteded to be merged into the response dict. Args: config (str): The vlan configuration block from the nodes running configuration Returns: dict: resource dict attribute """ value = STATE_RE.search(config).group('value') return dict(state=value) def _parse_trunk_groups(self, config): """ _parse_trunk_groups scans the provided configuration block and extracts all the vlan trunk groups. If no trunk groups are configured an empty List is returned as the vlaue. The return dict is intended to be merged into the response dict. Args: config (str): The vlan configuration block form the node's running configuration Returns: dict: resource dict attribute """ values = TRUNK_GROUP_RE.findall(config) return dict(trunk_groups=values) def getall(self): """Returns a dict object of all Vlans in the running-config Returns: A dict object of Vlan attributes """ # RE to find standalone and grouped (ranged, enumerated) vlans (#197) vlans_re = re.compile(r'(?<=^vlan\s)[\d,\-]+', re.M) response = dict() for vid in vlans_re.findall(self.config): response[vid] = self.get(vid) return response def create(self, vid): """ Creates a new VLAN resource Args: vid (str): The VLAN ID to create Returns: True if create was successful otherwise False """ command = 'vlan %s' % vid return self.configure(command) if isvlan(vid) else False def delete(self, vid): """ Deletes a VLAN from the running configuration Args: vid (str): The VLAN ID to delete Returns: True if the operation was successful otherwise False """ command = 'no vlan %s' % vid return self.configure(command) if isvlan(vid) else False def default(self, vid): """ Defaults the VLAN configuration .. code-block:: none default vlan Args: vid (str): The VLAN ID to default Returns: True if the operation was successful otherwise False """ command = 'default vlan %s' % vid return self.configure(command) if isvlan(vid) else False def configure_vlan(self, vid, commands): """ Configures the specified Vlan using commands Args: vid (str): The VLAN ID to configure commands: The list of commands to configure Returns: True if the commands completed successfully """ commands = make_iterable(commands) commands.insert(0, 'vlan %s' % vid) return self.configure(commands) def set_name(self, vid, name=None, default=False, disable=False): """ Configures the VLAN name EosVersion: 4.13.7M Args: vid (str): The VLAN ID to Configures name (str): The value to configure the vlan name default (bool): Defaults the VLAN ID name disable (bool): Negates the VLAN ID name Returns: True if the operation was successful otherwise False """ cmds = self.command_builder('name', value=name, default=default, disable=disable) return self.configure_vlan(vid, cmds) def set_state(self, vid, value=None, default=False, disable=False): """ Configures the VLAN state EosVersion: 4.13.7M Args: vid (str): The VLAN ID to configure value (str): The value to set the vlan state to default (bool): Configures the vlan state to its default value disable (bool): Negates the vlan state Returns: True if the operation was successful otherwise False """ cmds = self.command_builder('state', value=value, default=default, disable=disable) return self.configure_vlan(vid, cmds) def set_trunk_groups(self, vid, value=None, default=False, disable=False): """ Configures the list of trunk groups support on a vlan This method handles configuring the vlan trunk group value to default if the default flag is set to True. If the default flag is set to False, then this method will calculate the set of trunk group names to be added and to be removed. EosVersion: 4.13.7M Args: vid (str): The VLAN ID to configure value (str): The list of trunk groups that should be configured for this vlan id. default (bool): Configures the trunk group value to default if this value is true disable (bool): Negates the trunk group value if set to true Returns: True if the operation was successful otherwise False """ if default: return self.configure_vlan(vid, 'default trunk group') if disable: return self.configure_vlan(vid, 'no trunk group') current_value = self.get(vid)['trunk_groups'] failure = False value = make_iterable(value) for name in set(value).difference(current_value): if not self.add_trunk_group(vid, name): failure = True for name in set(current_value).difference(value): if not self.remove_trunk_group(vid, name): failure = True return not failure def add_trunk_group(self, vid, name): """ Adds a new trunk group to the Vlan in the running-config EosVersion: 4.13.7M Args: vid (str): The VLAN ID to configure name (str): The trunk group to add to the list Returns: True if the operation was successful otherwise False """ return self.configure_vlan(vid, 'trunk group %s' % name) def remove_trunk_group(self, vid, name): """ Removes a trunk group from the list of configured trunk groups for the specified VLAN ID EosVersion: 4.13.7M Args: vid (str): The VLAN ID to configure name (str): The trunk group to add to the list Returns: True if the operation was successful otherwise False """ return self.configure_vlan(vid, 'no trunk group %s' % name) def instance(node): """Returns an instance of Vlans This method will create and return an instance of the Vlans object passing the value of API to the object. The instance method is required for the resource to be autoloaded by the Node object Args: node (Node): The node argument passes an instance of Node to the resource """ return Vlans(node) pyeapi-1.0.2/pyeapi/api/vrfs.py0000644000076500000240000003012214447405743017543 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2017, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with EOS VRF resources The Vrfs resource provides configuration of VRF resources for an EOS node. Parameters: name (string): The name parameter maps to the VRF name in EOS. Valid values include any consecutive sequence of numbers, letters and underscore up to the maximum number of characters. This parameter is defaultable. description (string): The vrf description set by the user ipv4_routing (bool): Tells whether IPv4 routing is enabled on the VRF ipv6_routing (bool): Tells whether IPv6 unicast routing is enabled on the VRF """ import re from pyeapi.api import EntityCollection from pyeapi.utils import make_iterable RD_RE = re.compile(r'(?:\srd\s)(?P.*)$', re.M) DESCRIPTION_RE = re.compile(r'(?:description\s)(?P.*)$', re.M) class Vrfs(EntityCollection): """The Vrfs class provides a configuration resource for VRFs The Vrfs class is derived from ResourceBase a standard set of methods for working with VRF configurations on an EOS node. """ def get(self, value): """Returns the VRF configuration as a resource dict. Args: value (string): The vrf name to retrieve from the running configuration. Returns: A Python dict object containing the VRF attributes as key/value pairs. """ if self.version_number >= '4.23': config = self.get_block('vrf instance %s' % value) else: config = self.get_block('vrf definition %s' % value) if not config: return None response = dict(vrf_name=value) response.update(self._parse_rd(config)) response.update(self._parse_description(config)) config = self.get_block('no ip routing vrf %s' % value) if config: response['ipv4_routing'] = False else: response['ipv4_routing'] = True config = self.get_block('no ipv6 unicast-routing vrf %s' % value) if config: response['ipv6_routing'] = False else: response['ipv6_routing'] = True return response def _parse_rd(self, config): """ _parse_rd scans the provided configuration block and extracts the vrf rd. The return dict is intended to be merged into the response dict. Args: config (str): The vrf configuration block from the nodes running configuration Returns: dict: resource dict attribute """ match = RD_RE.search(config) if match: value = match.group('value') else: value = match return dict(rd=value) def _parse_description(self, config): """ _parse_description scans the provided configuration block and extracts the vrf description value. The return dict is intended to be merged into the response dict. Args: config (str): The vrf configuration block from the nodes running configuration Returns: dict: resource dict attribute """ value = DESCRIPTION_RE.search(config).group('value') return dict(description=value) def getall(self): """Returns a dict object of all VRFs in the running-config Returns: A dict object of VRF attributes """ if self.version_number >= '4.23': vrfs_re = re.compile(r'(?<=^vrf instance\s)(\w+)', re.M) else: vrfs_re = re.compile(r'(?<=^vrf definition\s)(\w+)', re.M) response = dict() for vrf in vrfs_re.findall(self.config): response[vrf] = self.get(vrf) return response def create(self, vrf_name, rd=None): """ Creates a new VRF resource Note: A valid RD has the following format admin_ID:local_assignment. The admin_ID can be an AS number or globally assigned IPv4 address. The local_assignment can be an integer between 0-65,535 if the admin_ID is an IPv4 address and can be between 0-4,294,967,295 if the admin_ID is an AS number. If the admin_ID is an AS number the local_assignment could also be in the form of an IPv4 address. Args: vrf_name (str): The VRF name to create rd (str): The value to configure the vrf rd Returns: True if create was successful otherwise False """ if self.version_number >= '4.23': commands = ['vrf instance %s' % vrf_name] else: commands = ['vrf definition %s' % vrf_name] if rd: commands.append('rd %s' % rd) return self.configure(commands) def delete(self, vrf_name): """ Deletes a VRF from the running configuration Args: vrf_name (str): The VRF name to delete Returns: True if the operation was successful otherwise False """ if self.version_number >= '4.23': command = 'no vrf instance %s' % vrf_name else: command = 'no vrf definition %s' % vrf_name return self.configure(command) def default(self, vrf_name): """ Defaults the VRF configuration for given name Args: vrf_name (str): The VRF name to default Returns: True if the operation was successful otherwise False """ if self.version_number >= '4.23': command = 'default vrf instance %s' % vrf_name else: command = 'default vrf definition %s' % vrf_name return self.configure(command) def configure_vrf(self, vrf_name, commands): """ Configures the specified VRF using commands Args: vrf_name (str): The VRF name to configure commands: The list of commands to configure Returns: True if the commands completed successfully """ commands = make_iterable(commands) if self.version_number >= '4.23': commands.insert(0, 'vrf instance %s' % vrf_name) else: commands.insert(0, 'vrf definition %s' % vrf_name) return self.configure(commands) def set_rd(self, vrf_name, rd): """ Configures the VRF rd (route distinguisher) Note: A valid RD has the following format admin_ID:local_assignment. The admin_ID can be an AS number or globally assigned IPv4 address. The local_assignment can be an integer between 0-65,535 if the admin_ID is an IPv4 address and can be between 0-4,294,967,295 if the admin_ID is an AS number. If the admin_ID is an AS number the local_assignment could also be in the form of an IPv4 address. Args: vrf_name (str): The VRF name to set rd for rd (str): The value to configure the vrf rd Returns: True if the operation was successful otherwise False """ cmds = self.command_builder('rd', value=rd) return self.configure_vrf(vrf_name, cmds) def set_description(self, vrf_name, description=None, default=False, disable=False): """ Configures the VRF description Args: vrf_name (str): The VRF name to configure description(str): The string to set the vrf description to default (bool): Configures the vrf description to its default value disable (bool): Negates the vrf description Returns: True if the operation was successful otherwise False """ cmds = self.command_builder('description', value=description, default=default, disable=disable) return self.configure_vrf(vrf_name, cmds) def set_ipv4_routing(self, vrf_name, default=False, disable=False): """ Configures ipv4 routing for the vrf Args: vrf_name (str): The VRF name to configure default (bool): Configures ipv4 routing for the vrf value to default if this value is true disable (bool): Negates the ipv4 routing for the vrf if set to true Returns: True if the operation was successful otherwise False """ cmd = 'ip routing vrf %s' % vrf_name if default: cmd = 'default %s' % cmd elif disable: cmd = 'no %s' % cmd cmd = make_iterable(cmd) return self.configure(cmd) def set_ipv6_routing(self, vrf_name, default=False, disable=False): """ Configures ipv6 unicast routing for the vrf Args: vrf_name (str): The VRF name to configure default (bool): Configures ipv6 unicast routing for the vrf value to default if this value is true disable (bool): Negates the ipv6 unicast routing for the vrf if set to true Returns: True if the operation was successful otherwise False """ cmd = 'ipv6 unicast-routing vrf %s' % vrf_name if default: cmd = 'default %s' % cmd elif disable: cmd = 'no %s' % cmd cmd = make_iterable(cmd) return self.configure(cmd) def set_interface(self, vrf_name, interface, default=False, disable=False): """ Adds a VRF to an interface Notes: Requires interface to be in routed mode. Must apply ip address after VRF has been applied. This feature can also be accessed through the interfaces api. Args: vrf_name (str): The VRF name to configure interface (str): The interface to add the VRF too default (bool): Set interface VRF forwarding to default disable (bool): Negate interface VRF forwarding Returns: True if the operation was successful otherwise False """ cmds = ['interface %s' % interface] if self.version_number >= '4.23': cmds.append(self.command_builder('vrf', value=vrf_name, default=default, disable=disable)) else: cmds.append(self.command_builder('vrf forwarding', value=vrf_name, default=default, disable=disable)) return self.configure(cmds) def instance(node): """Returns an instance of Vrfs This method will create and return an instance of the Vrfs object passing the value of API to the object. The instance method is required for the resource to be autoloaded by the Node object Args: node (Node): The node argument passes an instance of Node to the resource """ return Vrfs(node) pyeapi-1.0.2/pyeapi/api/vrrp.py0000644000076500000240000016712014447405743017565 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Module for working with EOS VRRP resources The Vrrp resource provides configuration management of interface specific vrrp resources on and EOS node. It provides the following class implementations: * Vrrp - Configure vrrps in EOS Vrrp Attributes: - enable (boolean): The shutdown state of the vrrp - primary_ip (string): The ip address of the vrrp - secondary_ip (dict): The secondary ip addresses configured for the vrrp This is a dictionary in the format:: { key: [ list of ip addresses ] } where key is 'add', 'remove', or 'exists'. 'add' is used to add the list of secondary ip addresses to the vrrp. 'remove' will remove the list of secondary ip addresses from the vrrp. 'exists' is a report only key for retrieving the current secondary ip addresses on a vrrp. - priority (int): The priority rank of the vrrp - description (string): The description for the vrrp - ip_version (int): The ip version value for the vrrp - timers_advertise (int): The timers advertise setting for the vrrp - mac_addr_adv_interval (int): The mac-address advertisement-interval setting for the vrrp - preempt (boolean): The preempt state of the vrrp - preempt_delay_min (int): The preempt delay minimum setting for the vrrp - preempt_delay_reload (int): The preempt delay reload setting for the vrrp - delay_reload (int): The delay reload setting for the vrrp - track (list): The object tracking settings for the vrrp - bfd_ip (string): The bfd ip set for the vrrp Notes: The get method will return a dictionary of all the currently configured vrrps on a single interface, with the VRID of each vrrp as the keys in the dictionary:: { vrrp1: { data }, vrrp2: { data }, } The getall method will return a dictionary of all the currently configured vrrps on the node, with the interface name as the top-level keys, with the VRIDs for each vrrp on an interface as a sub-key of that interface:: { interface1: { vrrp1: { data }, vrrp2: { data }, }, interface2: { vrrp1: { data }, vrrp2: { data }, } } The data for a configured vrrp is a dictionary with the following format:: { enable: primary_ip: priority: description: secondary_ip: { exists: [ , ] } ip_version: timers_advertise: mac_addr_adv_interval: preempt: preempt_delay_min: preempt_delay_reload: delay_reload: track: [ { name: action: amount: |default|no|None }, { name: action: amount: |default|no|None }, ] bfd_ip: } The create and method accepts a kwargs dictionary which defines the properties to be applied to the new or existing vrrp configuration. The available keywords and values are as follows: - enable: True to enable (no shutdown)|False to disable (shutdown) - primary_ip: |no|default|None - priority: |no|default|None - description: |no|default|None - secondary_ip: may include the following - add: - remove: - ip_version: |no|default|None - timers_advertise: |no|default|None - mac_addr_adv_interval: |no|default|None - preempt: True to enable (preempt)|False to disable (no preempt) - preempt_delay_min: |no|default|None - preempt_delay_reload: |no|default|None - delay_reload: |no|default|None - track: of dicts in the following format:: { name: action: amount: |default|no|None } - bfd_ip: |no|default|None """ import re from pyeapi.api import EntityCollection PROPERTIES = ['primary_ip', 'priority', 'description', 'secondary_ip', 'ip_version', 'enable', 'timers_advertise', 'mac_addr_adv_interval', 'preempt', 'preempt_delay_min', 'preempt_delay_reload', 'delay_reload', 'track', 'bfd_ip'] class Vrrp(EntityCollection): """The Vrrp class provides management of the VRRP configuration The Vrrp class is derived from EntityCollection and provides an API for working with the node's vrrp configurations. """ def get(self, name): """Get the vrrp configurations for a single node interface Args: name (string): The name of the interface for which vrrp configurations will be retrieved. Returns: A dictionary containing the vrrp configurations on the interface. Returns None if no vrrp configurations are defined or if the interface is not configured. """ # Validate the interface and vrid are specified interface = name if not interface: raise ValueError("Vrrp.get(): interface must contain a value.") # Get the config for the interface. Return None if the # interface is not defined config = self.get_block('interface %s' % interface) if config is None: return config # Find all occurrences of vrids in this interface and make # a set of the unique vrid numbers match = set(re.findall(r'^\s+(?:no |)vrrp (\d+)', config, re.M)) if not match: return None # Initialize the result dict result = dict() for vrid in match: subd = dict() # Parse the vrrp configuration for the vrid(s) in the list subd.update(self._parse_delay_reload(config, vrid)) subd.update(self._parse_description(config, vrid)) subd.update(self._parse_enable(config, vrid)) subd.update(self._parse_ip_version(config, vrid)) subd.update(self._parse_mac_addr_adv_interval(config, vrid)) subd.update(self._parse_preempt(config, vrid)) subd.update(self._parse_preempt_delay_min(config, vrid)) subd.update(self._parse_preempt_delay_reload(config, vrid)) subd.update(self._parse_primary_ip(config, vrid)) subd.update(self._parse_priority(config, vrid)) subd.update(self._parse_secondary_ip(config, vrid)) subd.update(self._parse_timers_advertise(config, vrid)) subd.update(self._parse_track(config, vrid)) subd.update(self._parse_bfd_ip(config, vrid)) result.update({int(vrid): subd}) # If result dict is empty, return None, otherwise return result return result if result else None def getall(self): """Get the vrrp configurations for all interfaces on a node Returns: A dictionary containing the vrrp configurations on the node, keyed by interface. """ vrrps = dict() # Find the available interfaces interfaces = re.findall(r'^interface\s(\S+)', self.config, re.M) # Get the vrrps defined for each interface for interface in interfaces: vrrp = self.get(interface) # Only add those interfaces that have vrrps defined if vrrp: vrrps.update({interface: vrrp}) return vrrps def _parse_enable(self, config, vrid): if self.version_number >= '4.21.3': match = re.search(r'^\s+vrrp %s disabled$' % vrid, config, re.M) else: match = re.search(r'^\s+vrrp %s shutdown$' % vrid, config, re.M) if match: return dict(enable=False) return dict(enable=True) def _parse_primary_ip(self, config, vrid): if self.version_number >= '4.21.3': match = re.search(r'^\s+vrrp %s ipv4 (\d+\.\d+\.\d+\.\d+)$' % vrid, config, re.M) else: match = re.search(r'^\s+vrrp %s ip (\d+\.\d+\.\d+\.\d+)$' % vrid, config, re.M) value = match.group(1) if match else None return dict(primary_ip=value) def _parse_priority(self, config, vrid): if self.version_number >= '4.21.3': match = re.search(r'^\s+vrrp %s priority-level (\d+)$' % vrid, config, re.M) else: match = re.search(r'^\s+vrrp %s priority (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(priority=value) def _parse_timers_advertise(self, config, vrid): if self.version_number >= '4.21.3': match = re.search(r'^\s+vrrp %s advertisement interval (\d+)$' % vrid, config, re.M) else: match = re.search(r'^\s+vrrp %s timers advertise (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(timers_advertise=value) def _parse_preempt(self, config, vrid): match = re.search(r'^\s+vrrp %s preempt$' % vrid, config, re.M) if match: return dict(preempt=True) return dict(preempt=False) def _parse_secondary_ip(self, config, vrid): if self.version_number >= '4.21.3': matches = re.findall(r'^\s+vrrp %s ipv4 (\d+\.\d+\.\d+\.\d+) ' r'secondary$' % vrid, config, re.M) else: matches = re.findall(r'^\s+vrrp %s ip (\d+\.\d+\.\d+\.\d+) ' r'secondary$' % vrid, config, re.M) value = matches if matches else [] return dict(secondary_ip=value) def _parse_description(self, config, vrid): if self.version_number >= '4.21.3': match = re.search(r'^\s+vrrp %s session description(.*)$' % vrid, config, re.M) else: match = re.search(r'^\s+vrrp %s description(.*)$' % vrid, config, re.M) if match: return dict(description=match.group(1).lstrip()) return dict(description='') def _parse_mac_addr_adv_interval(self, config, vrid): match = re.search(r'^\s+vrrp %s mac-address advertisement-interval ' r'(\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(mac_addr_adv_interval=value) def _parse_preempt_delay_min(self, config, vrid): match = re.search(r'^\s+vrrp %s preempt delay minimum (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(preempt_delay_min=value) def _parse_preempt_delay_reload(self, config, vrid): match = re.search(r'^\s+vrrp %s preempt delay reload (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(preempt_delay_reload=value) def _parse_bfd_ip(self, config, vrid): match = re.search(r'^\s+vrrp %s bfd ip' r'(?: (\d+\.\d+\.\d+\.\d+)|)$' % vrid, config, re.M) if match: return dict(bfd_ip=match.group(1)) return dict(bfd_ip='') def _parse_ip_version(self, config, vrid): if self.version_number >= '4.21.3': match = re.search(r'^\s+vrrp %s ipv4 version (\d+)$' % vrid, config, re.M) else: match = re.search(r'^\s+vrrp %s ip version (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(ip_version=value) def _parse_delay_reload(self, config, vrid): if self.version_number >= '4.21.3': match = re.search(r'^\s+vrrp %s timers delay reload (\d+)$' % vrid, config, re.M) else: match = re.search(r'^\s+vrrp %s delay reload (\d+)$' % vrid, config, re.M) value = int(match.group(1)) if match else None return dict(delay_reload=value) def _parse_track(self, config, vrid): if self.version_number >= '4.21.3': matches = re.findall(r'^\s+vrrp %s tracked-object (\S+) ' r'(decrement|shutdown)(?:( \d+$|$))' % vrid, config, re.M) else: matches = re.findall(r'^\s+vrrp %s track (\S+) ' r'(decrement|shutdown)(?:( \d+$|$))' % vrid, config, re.M) value = [] for match in matches: tr_obj = match[0] action = match[1] amount = None if match[2] == '' else int(match[2]) entry = { 'name': tr_obj, 'action': action, } if amount: entry.update({'amount': amount}) value.append(entry) # Return the list, sorted for easier comparison track_list = sorted(value, key=lambda k: (k['name'], k['action'])) return dict(track=track_list) def create(self, interface, vrid, **kwargs): """Creates a vrrp instance from an interface Note: This method will attempt to create a vrrp in the node's operational config. If the vrrp already exists on the interface, then this method will set the properties of the existing vrrp to those that have been passed in, if possible. Args: interface (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be created. kwargs (dict): A dictionary specifying the properties to be applied to the new vrrp instance. See library documentation for available keys and values. Returns: True if the vrrp could be created otherwise False (see Node) """ if 'enable' not in kwargs: kwargs['enable'] = False return self._vrrp_set(interface, vrid, **kwargs) def delete(self, interface, vrid): """Deletes a vrrp instance from an interface Note: This method will attempt to delete the vrrp from the node's operational config. If the vrrp does not exist on the interface then this method will not perform any changes but still return True Args: interface (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be deleted. Returns: True if the vrrp could be deleted otherwise False (see Node) """ vrrp_str = "no vrrp %d" % vrid return self.configure_interface(interface, vrrp_str) def default(self, interface, vrid): """Defaults a vrrp instance from an interface Note: This method will attempt to default the vrrp on the node's operational config. Default results in the deletion of the specified vrrp . If the vrrp does not exist on the interface then this method will not perform any changes but still return True Args: interface (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be defaulted. Returns: True if the vrrp could be defaulted otherwise False (see Node) """ vrrp_str = "default vrrp %d" % vrid return self.configure_interface(interface, vrrp_str) def set_enable(self, name, vrid, value=False, run=True): """Set the enable property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (boolean): True to enable the vrrp, False to disable. run (boolean): True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure If run is False, returns the formatted command string which can be passed to the node """ if value is False: if self.version_number >= '4.21.3': cmd = "vrrp %d disabled" % vrid else: cmd = "vrrp %d shutdown" % vrid elif value is True: if self.version_number >= '4.21.3': cmd = "no vrrp %d disabled" % vrid else: cmd = "no vrrp %d shutdown" % vrid else: raise ValueError("vrrp property 'enable' must be " "True or False") # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_primary_ip(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the primary_ip property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (string): IP address to be set. disable (boolean): Unset primary ip if True. default (boolean): Set primary ip to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if default is True: vrrps = self.get(name) primary_ip = vrrps[vrid]['primary_ip'] if self.version_number >= '4.21.3': cmd = "default vrrp %d ipv4 %s" % (vrid, primary_ip) else: cmd = "default vrrp %d ip %s" % (vrid, primary_ip) elif disable is True or value is None: vrrps = self.get(name) primary_ip = vrrps[vrid]['primary_ip'] if self.version_number >= '4.21.3': cmd = "no vrrp %d ipv4 %s" % (vrid, primary_ip) else: cmd = "no vrrp %d ip %s" % (vrid, primary_ip) elif re.match(r'^\d+\.\d+\.\d+\.\d+$', str(value)): if self.version_number >= '4.21.3': cmd = "vrrp %d ipv4 %s" % (vrid, value) else: cmd = "vrrp %d ip %s" % (vrid, value) else: raise ValueError("vrrp property 'primary_ip' must be " "a properly formatted IP address") # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_priority(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the primary_ip property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (integer): Priority to assign to the vrrp. disable (boolean): Unset priority if True. default (boolean): Set priority to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if not default and not disable: if not str(value).isdigit() or value < 1 or value > 254: raise ValueError("vrrp property 'priority' must be " "an integer in the range 1-254") if self.version_number >= '4.21.3': cmd = self.command_builder('vrrp %d priority-level' % vrid, value=value, default=default, disable=disable) else: cmd = self.command_builder('vrrp %d priority' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_description(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the description property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (string): Description to assign to the vrrp. disable (boolean): Unset description if True. default (boolean): Set description to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if self.version_number >= '4.21.3': cmd = self.command_builder('vrrp %d session description' % vrid, value=value, default=default, disable=disable) else: cmd = self.command_builder('vrrp %d description' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_ip_version(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the ip_version property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (integer): IP version to assign to the vrrp. disable (boolean): Unset ip_version if True. default (boolean): Set ip_version to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if not default and not disable: if value not in (2, 3): raise ValueError("vrrp property 'ip_version' must be 2 or 3") if self.version_number >= '4.21.3': cmd = self.command_builder('vrrp %d ipv4 version' % vrid, value=value, default=default, disable=disable) else: cmd = self.command_builder('vrrp %d ip version' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_secondary_ips(self, name, vrid, secondary_ips, run=True): """Configure the secondary_ip property of the vrrp Notes: set_secondary_ips takes a list of secondary ip addresses which are to be set on the virtal router. An empty list will remove any existing secondary ip addresses from the vrrp. A list containing addresses will configure the virtual router with only the addresses specified in the list - any existing addresses not included in the list will be removed. Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. secondary_ips (list): A list of secondary ip addresses to be assigned to the virtual router. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ cmds = [] # Get the current set of tracks defined for the vrrp curr_sec_ips = [] vrrps = self.get(name) if vrrps and vrid in vrrps: curr_sec_ips = vrrps[vrid]['secondary_ip'] # Validate the list of ip addresses for sec_ip in secondary_ips: if type(sec_ip) is not str or \ not re.match(r'^\d+\.\d+\.\d+\.\d+$', sec_ip): raise ValueError("vrrp property 'secondary_ip' must be a list " "of properly formatted ip address strings") intersection = list(set(curr_sec_ips) & set(secondary_ips)) # Delete the intersection from both lists to determine which # addresses need to be added or removed from the vrrp remove = list(set(curr_sec_ips) - set(intersection)) add = list(set(secondary_ips) - set(intersection)) # Build the commands to add and remove the secondary ip addresses for sec_ip in remove: if self.version_number >= '4.21.3': cmds.append("no vrrp %d ipv4 %s secondary" % (vrid, sec_ip)) else: cmds.append("no vrrp %d ip %s secondary" % (vrid, sec_ip)) for sec_ip in add: if self.version_number >= '4.21.3': cmds.append("vrrp %d ipv4 %s secondary" % (vrid, sec_ip)) else: cmds.append("vrrp %d ip %s secondary" % (vrid, sec_ip)) cmds = sorted(cmds) # Run the command if requested if run: result = self.configure_interface(name, cmds) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmds def set_timers_advertise(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the ip_version property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (integer): Timers advertise value to assign to the vrrp. disable (boolean): Unset timers advertise if True. default (boolean): Set timers advertise to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if not default and not disable: if not int(value) or int(value) < 1 or int(value) > 255: raise ValueError("vrrp property 'timers_advertise' must be" "in the range 1-255") if self.version_number >= '4.21.3': cmd = self.command_builder('vrrp %d advertisement interval' % vrid, value=value, default=default, disable=disable) else: cmd = self.command_builder('vrrp %d timers advertise' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_mac_addr_adv_interval(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the mac_addr_adv_interval property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (integer): mac-address advertisement-interval value to assign to the vrrp. disable (boolean): Unset mac-address advertisement-interval if True. default (boolean): Set mac-address advertisement-interval to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if not default and not disable: if not int(value) or int(value) < 1 or int(value) > 3600: raise ValueError("vrrp property 'mac_addr_adv_interval' must " "be in the range 1-3600") cmd = self.command_builder('vrrp %d mac-address advertisement-interval' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_preempt(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the preempt property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (boolean): True to enable preempt, False to disable preempt on the vrrp. disable (boolean): Unset preempt if True. default (boolean): Set preempt to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if not default and not disable: if value is not True and value is not False: raise ValueError("vrrp property 'preempt' must be True " "or False") cmd = self.command_builder('vrrp %d preempt' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_preempt_delay_min(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the preempt_delay_min property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (integer): Preempt delay minimum value to set on the vrrp. disable (boolean): Unset preempt delay minimum if True. default (boolean): Set preempt delay minimum to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if not default and not disable: if not int(value) or int(value) < 1 or int(value) > 3600: raise ValueError("vrrp property 'preempt_delay_min' must be" "in the range 0-3600 %r" % value) cmd = self.command_builder('vrrp %d preempt delay minimum' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_preempt_delay_reload(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the preempt_delay_min property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (integer): Preempt delay reload value to set on the vrrp. disable (boolean): Unset preempt delay reload if True. default (boolean): Set preempt delay reload to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if not default and not disable: if not int(value) or int(value) < 1 or int(value) > 3600: raise ValueError("vrrp property 'preempt_delay_reload' must be" "in the range 0-3600 %r" % value) cmd = self.command_builder('vrrp %d preempt delay reload' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_delay_reload(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the preempt_delay_min property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (integer): Preempt delay reload value to set on the vrrp. disable (boolean): Unset preempt delay reload if True. default (boolean): Set preempt delay reload to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if not default and not disable: if not int(value) or int(value) < 1 or int(value) > 3600: raise ValueError("vrrp property 'delay_reload' must be" "in the range 0-3600 %r" % value) if self.version_number >= '4.21.3': cmd = self.command_builder('vrrp %d timers delay reload' % vrid, value=value, default=default, disable=disable) else: cmd = self.command_builder('vrrp %d delay reload' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def set_tracks(self, name, vrid, tracks, run=True): """Configure the track property of the vrrp Notes: set_tracks takes a list of tracked objects which are to be set on the virtual router. An empty list will remove any existing tracked objects from the vrrp. A list containing track entries configures the virtual router to track only the objects specified in the list - any existing tracked objects on the vrrp not included in the list will be removed. Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. tracks (list): A list of track definition dictionaries. Each dictionary is a definition of a tracked object in one of the two formats:: {'name': tracked_object_name, 'action': 'shutdown'} {'name': tracked_object_name, 'action': 'decrement', 'amount': amount_of_decrement} run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ cmds = [] # Get the current set of tracks defined for the vrrp curr_tracks = [] vrrps = self.get(name) if vrrps and vrid in vrrps: curr_tracks = vrrps[vrid]['track'] # Determine which tracked objects are in both lists using # sets of temporary strings built from the track specifications unset = '_none_' tracks_set = [] for track in tracks: keys = track.keys() # Validate no extraneous keys in track definition err_keys = set(keys).difference(('name', 'action', 'amount')) if err_keys: err_keys = ', '.join(err_keys) raise ValueError("Error found in vrrp property 'track': " "unknown key(s) '%s' found. Valid keys are " "name, action, and amount" % err_keys) # Validate required keys in track definition if not set(keys).issuperset(('name', 'action')): raise ValueError("Error found in vrrp property 'track': " "track definition must contain 'name' and " "'action' keys") tr_obj = track['name'] action = track['action'] amount = track['amount'] if 'amount' in track else unset # Validate values in track definition error = False if action not in ('shutdown', 'decrement'): error = True if action == 'shutdown' and amount != unset: error = True if amount != unset and not str(amount).isdigit(): error = True if error: raise ValueError("Error found in vrrp property 'track'. " "See documentation for format specification.") tid = "%s %s %s" % (tr_obj, action, amount) tracks_set.append(tid) curr_set = [] for track in curr_tracks: tr_obj = track['name'] action = track['action'] amount = track['amount'] if 'amount' in track else unset # Validate track definition error = False if action not in ('shutdown', 'decrement'): error = True if action == 'shutdown' and amount != unset: error = True if amount != unset and not str(amount).isdigit(): error = True if error: raise ValueError("Error found in vrrp property 'track'. " "See documentation for format specification.") tid = "%s %s %s" % (tr_obj, action, amount) curr_set.append(tid) intersection = list(set(tracks_set) & set(curr_set)) # Delete the intersection from both lists to determine which # track definitions need to be added or removed from the vrrp remove = list(set(curr_set) - set(intersection)) add = list(set(tracks_set) - set(intersection)) # Build the commands to add and remove the tracked objects for track in remove: match = re.match(r'(\S+)\s+(\S+)\s+(\S+)', track) if match: (tr_obj, action, amount) = \ (match.group(1), match.group(2), match.group(3)) if amount == unset: amount = '' if self.version_number >= '4.21.3': t_cmd = ("no vrrp %d tracked-object %s %s %s" % (vrid, tr_obj, action, amount)) else: t_cmd = ("no vrrp %d track %s %s %s" % (vrid, tr_obj, action, amount)) cmds.append(t_cmd.rstrip()) for track in add: match = re.match(r'(\S+)\s+(\S+)\s+(\S+)', track) if match: (tr_obj, action, amount) = \ (match.group(1), match.group(2), match.group(3)) if amount == unset: amount = '' if self.version_number >= '4.21.3': t_cmd = ("vrrp %d tracked-object %s %s %s" % (vrid, tr_obj, action, amount)) else: t_cmd = ("vrrp %d track %s %s %s" % (vrid, tr_obj, action, amount)) cmds.append(t_cmd.rstrip()) cmds = sorted(cmds) # Run the command if requested if run: result = self.configure_interface(name, cmds) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmds def set_bfd_ip(self, name, vrid, value=None, disable=False, default=False, run=True): """Set the bfd_ip property of the vrrp Args: name (string): The interface to configure. vrid (integer): The vrid number for the vrrp to be managed. value (string): The bfd ip address to be set. disable (boolean): Unset bfd ip if True. default (boolean): Set bfd ip to default if True. run (boolean): Set to True to execute the command, False to return a string with the formatted command. Returns: If run is True, returns True if the command executed successfully, error if failure. If run is False, returns the formatted command string which can be passed to the node """ if not default and not disable: if not re.match(r'^\d+\.\d+\.\d+\.\d+$', str(value)): raise ValueError("vrrp property 'bfd_ip' must be " "a properly formatted IP address") cmd = self.command_builder('vrrp %d bfd ip' % vrid, value=value, default=default, disable=disable) # Run the command if requested if run: result = self.configure_interface(name, cmd) # And verify the command succeeded if result is False: return self.error return result # Otherwise return the formatted command return cmd def _vrrp_set(self, name, vrid, **kwargs): # Configure the commands to create or update a vrrp # configuration, and send the commands to the node. vrconf = kwargs # step through the individual vrrp properties and # set those that need to be changed commands = [] enable = vrconf.get('enable', '__NONE__') if enable != '__NONE__': cmd = self.set_enable(name, vrid, value=enable, run=False) commands.append(cmd) primary_ip = vrconf.get('primary_ip', '__NONE__') if primary_ip != '__NONE__': if primary_ip in ('no', None): cmd = self.set_primary_ip(name, vrid, value=None, disable=True, run=False) elif primary_ip == 'default': cmd = self.set_primary_ip(name, vrid, value=None, default=True, run=False) else: cmd = self.set_primary_ip(name, vrid, value=primary_ip, run=False) commands.append(cmd) priority = vrconf.get('priority', '__NONE__') if priority != '__NONE__': if priority in ('no', None): cmd = self.set_priority(name, vrid, value=priority, disable=True, run=False) elif priority == 'default': cmd = self.set_priority(name, vrid, value=priority, default=True, run=False) else: cmd = self.set_priority(name, vrid, value=priority, run=False) commands.append(cmd) description = vrconf.get('description', '__NONE__') if description != '__NONE__': if description in ('no', None): cmd = self.set_description(name, vrid, value=description, disable=True, run=False) elif description == 'default': cmd = self.set_description(name, vrid, value=description, default=True, run=False) else: cmd = self.set_description(name, vrid, value=description, run=False) commands.append(cmd) ip_version = vrconf.get('ip_version', '__NONE__') if ip_version != '__NONE__': if ip_version in ('no', None): cmd = self.set_ip_version(name, vrid, value=ip_version, disable=True, run=False) elif ip_version == 'default': cmd = self.set_ip_version(name, vrid, value=ip_version, default=True, run=False) else: cmd = self.set_ip_version(name, vrid, value=ip_version, run=False) commands.append(cmd) secondary_ip = vrconf.get('secondary_ip', '__NONE__') if secondary_ip != '__NONE__': cmds = self.set_secondary_ips(name, vrid, secondary_ip, run=False) for cmd in cmds: commands.append(cmd) timers_advertise = vrconf.get('timers_advertise', '__NONE__') if timers_advertise != '__NONE__': if timers_advertise in ('no', None): cmd = self.set_timers_advertise(name, vrid, value=timers_advertise, disable=True, run=False) elif timers_advertise == 'default': cmd = self.set_timers_advertise(name, vrid, value=timers_advertise, default=True, run=False) else: cmd = self.set_timers_advertise(name, vrid, value=timers_advertise, run=False) commands.append(cmd) mac_addr_adv_interval = \ vrconf.get('mac_addr_adv_interval', '__NONE__') if mac_addr_adv_interval != '__NONE__': if mac_addr_adv_interval in ('no', None): cmd = \ self.set_mac_addr_adv_interval(name, vrid, value=mac_addr_adv_interval, disable=True, run=False) elif mac_addr_adv_interval == 'default': cmd = \ self.set_mac_addr_adv_interval(name, vrid, value=mac_addr_adv_interval, default=True, run=False) else: cmd = \ self.set_mac_addr_adv_interval(name, vrid, value=mac_addr_adv_interval, run=False) commands.append(cmd) preempt = vrconf.get('preempt', '__NONE__') if preempt != '__NONE__': if preempt in ('no', False): cmd = self.set_preempt(name, vrid, value=preempt, disable=True, run=False) elif preempt == 'default': cmd = self.set_preempt(name, vrid, value=preempt, default=True, run=False) else: cmd = self.set_preempt(name, vrid, value=preempt, run=False) commands.append(cmd) preempt_delay_min = vrconf.get('preempt_delay_min', '__NONE__') if preempt_delay_min != '__NONE__': if preempt_delay_min in ('no', None): cmd = self.set_preempt_delay_min(name, vrid, value=preempt_delay_min, disable=True, run=False) elif preempt_delay_min == 'default': cmd = self.set_preempt_delay_min(name, vrid, value=preempt_delay_min, default=True, run=False) else: cmd = self.set_preempt_delay_min(name, vrid, value=preempt_delay_min, run=False) commands.append(cmd) preempt_delay_reload = vrconf.get('preempt_delay_reload', '__NONE__') if preempt_delay_reload != '__NONE__': if preempt_delay_reload in ('no', None): cmd = self.set_preempt_delay_reload(name, vrid, value=preempt_delay_reload, disable=True, run=False) elif preempt_delay_reload == 'default': cmd = self.set_preempt_delay_reload(name, vrid, value=preempt_delay_reload, default=True, run=False) else: cmd = self.set_preempt_delay_reload(name, vrid, value=preempt_delay_reload, run=False) commands.append(cmd) delay_reload = vrconf.get('delay_reload', '__NONE__') if delay_reload != '__NONE__': if delay_reload in ('no', None): cmd = self.set_delay_reload(name, vrid, value=delay_reload, disable=True, run=False) elif delay_reload == 'default': cmd = self.set_delay_reload(name, vrid, value=delay_reload, default=True, run=False) else: cmd = self.set_delay_reload(name, vrid, value=delay_reload, run=False) commands.append(cmd) track = vrconf.get('track', '__NONE__') if track != '__NONE__': cmds = self.set_tracks(name, vrid, track, run=False) for cmd in cmds: commands.append(cmd) bfd_ip = vrconf.get('bfd_ip', '__NONE__') if bfd_ip != '__NONE__': if bfd_ip in ('no', None): cmd = self.set_bfd_ip(name, vrid, value=bfd_ip, disable=True, run=False) elif bfd_ip == 'default': cmd = self.set_bfd_ip(name, vrid, value=bfd_ip, default=True, run=False) else: cmd = self.set_bfd_ip(name, vrid, value=bfd_ip, run=False) commands.append(cmd) # Send the commands to the requested interface result = self.configure_interface(name, commands) # And verify the commands succeeded if result is False: return self.error return result def vrconf_format(self, vrconfig): """Formats a vrrp configuration dictionary to match the information as presented from the get and getall methods. vrrp configuration dictionaries passed to the create method may contain data for setting properties which results in a default value on the node. In these instances, the data for setting or changing the property is replaced with the value that would be returned from the get and getall methods. Intended for validating updated vrrp configurations. """ fixed = dict(vrconfig) # primary_ip: default, no, None results in address of 0.0.0.0 if fixed['primary_ip'] in ('no', 'default', None): fixed['primary_ip'] = '0.0.0.0' # priority: default, no, None results in priority of 100 if fixed['priority'] in ('no', 'default', None): fixed['priority'] = 100 # description: default, no, None results in None if fixed['description'] in ('no', 'default', None): fixed['description'] = None # secondary_ip: list should be exactly what is required, # just sort it for easier comparison if 'secondary_ip' in fixed: fixed['secondary_ip'] = sorted(fixed['secondary_ip']) # ip_version: default, no, None results in value of 2 if fixed['ip_version'] in ('no', 'default', None): fixed['ip_version'] = 2 # timers_advertise: default, no, None results in value of 1 if fixed['timers_advertise'] in ('no', 'default', None): fixed['timers_advertise'] = 1 # mac_address_advertisement_interaval: # default, no, None results in value of 30 if fixed['mac_addr_adv_interval'] in \ ('no', 'default', None): fixed['mac_addr_adv_interval'] = 30 # preempt: default, no results in value of False if fixed['preempt'] in ('no', 'default'): fixed['preempt'] = False # preempt_delay_min: default, no, None results in value of 0 if fixed['preempt_delay_min'] in ('no', 'default', None): fixed['preempt_delay_min'] = 0 # preempt_delay_reload: default, no, None results in value of 0 if fixed['preempt_delay_reload'] in ('no', 'default', None): fixed['preempt_delay_reload'] = 0 # delay_reload: default, no, None results in value of 0 if fixed['delay_reload'] in ('no', 'default', None): fixed['delay_reload'] = 0 # track: list should be exactly what is required, # just sort it for easier comparison if 'track' in fixed: fixed['track'] = \ sorted(fixed['track'], key=lambda k: (k['name'], k['action'])) # bfd_ip: default, no, None results in '' if fixed['bfd_ip'] in ('no', 'default', None): fixed['bfd_ip'] = '' return fixed def instance(node): """Returns an instance of Vrrp Args: node (Node): The node argument passes an instance of Node to the resource Returns: object: An instance of Vrrp """ return Vrrp(node) pyeapi-1.0.2/pyeapi/client.py0000644000076500000240000012022714447405743017276 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Python Client for eAPI This module provides the client for eAPI. It provides the primary functions for building applications that work with Arista EOS eAPI-enabled nodes. The first function is to provide a client for sending and receiving eAPI request and response objects on a per node basis. The second function provides a library for building API enabled data models for configuring EOS nodes. This library allows for creating connections to EOS eAPI enabled nodes using the connect or connect_to function. Both functions will return an instance of a Node object that can be used to send and receive eAPI commands. The Node object can autoload API modules for a structured object oriented approach to configuring the EOS node with native Python objects. Example: >>> import pyeapi >>> conn = pyeapi.connect(host='10.1.1.1', transport='http') >>> conn.execute(['show version']) {u'jsonrpc': u'2.0', u'result': [{u'memTotal': 2028008, u'version': u'4.14.5F', u'internalVersion': u'4.14.5F-2209869.4145F', u'serialNumber': u'', u'systemMacAddress': u'00:0c:29:f5:d2:7d', u'bootupTimestamp': 1421765066.11, u'memFree': 213212, u'modelName': u'vEOS', u'architecture': u'i386', u'internalBuildId': u'f590eed4-1e66-43c6-8943-cee0390fbafe', u'hardwareRevision': u''}], u'id': u'4312565648'} >>> node = pyeapi.connect_to('veos01') >>> node.enable('show version') {u'jsonrpc': u'2.0', u'result': [{u'memTotal': 2028008, u'version': u'4.14.5F', u'internalVersion': u'4.14.5F-2209869.4145F', u'serialNumber': u'', u'systemMacAddress': u'00:0c:29:f5:d2:7d', u'bootupTimestamp': 1421765066.11, u'memFree': 213212, u'modelName': u'vEOS', u'architecture': u'i386', u'internalBuildId': u'f590eed4-1e66-43c6-8943-cee0390fbafe', u'hardwareRevision': u''}], u'id': u'4312565648'} Additionally the node object can automatically load API modules to work with the resources in the configuration. The API autoloader supports automatic loading of modules in pyeapi.api as well as provides the ability to build custom API modules to be loaded from a different namespace. Example: >>> import pyeapi >>> node = pyeapi.connect_to('veos01') >>> node.api('vlans').get(1) {'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []} The API autoloader loads API modules by their filename. The following objects are provide in this module for creating clients to interface with eAPI. Node -- Creates an instance of a node object that represents a single EOS device. Each EOS device to be managed should have a Node instance Config -- A subclass of ConfigParser.SafeConfigParser that handles the configuration file. The configuration file is an INI style file that contains the settings for nodes used by the connect_to function. """ from uuid import uuid4 import os import re from functools import lru_cache # Note: SafeConfigParser is deprecated and replaced by ConfigParser from configparser import ConfigParser as SafeConfigParser from configparser import Error as SafeConfigParserError from pyeapi.utils import load_module, make_iterable, debug, CliVariants from pyeapi.eapilib import HttpEapiConnection, HttpsEapiConnection from pyeapi.eapilib import HttpsEapiCertConnection from pyeapi.eapilib import HttpEapiSessionConnection from pyeapi.eapilib import HttpsEapiSessionConnection from pyeapi.eapilib import SocketEapiConnection, HttpLocalEapiConnection from pyeapi.eapilib import CommandError CONFIG_SEARCH_PATH = ['~/.eapi.conf', '/mnt/flash/eapi.conf'] TRANSPORTS = { 'socket': SocketEapiConnection, 'http_local': HttpLocalEapiConnection, 'http': HttpEapiConnection, 'http_session': HttpEapiSessionConnection, 'https': HttpsEapiConnection, 'https_certs': HttpsEapiCertConnection, 'https_session': HttpsEapiSessionConnection, } DEFAULT_TRANSPORT = 'https' class Config(SafeConfigParser): """Conifguration instance for managing the eapi.conf file. This class provides an instance for handling the configuration file. It should normally need to be instantiated. A single config object is instantiated by the module for working with the config. Attributes: filename (str): The full path to the loaded filename Args: filename(str): The full path to the filename to be loaded when the object is instantiated. """ def __init__(self, filename=None): SafeConfigParser.__init__(self) self.filename = filename self.tags = dict() self.autoload() @property def connections(self): """ Returns all of the loaded connections names as a list """ conn = lambda x: str(x).replace('connection:', '') return [conn(name) for name in self.sections()] def autoload(self): """ Loads the eapi.conf file This method will use the module variable CONFIG_SEARCH_PATH to attempt to locate a valid eapi.conf file if a filename is not already configured. This method will load the first eapi.conf file it finds and then return. The CONFIG_SEARCH_PATH can be overridden using an environment variable by setting EAPI_CONF. """ path = list(CONFIG_SEARCH_PATH) if 'EAPI_CONF' in os.environ: path = os.environ['EAPI_CONF'] elif self.filename: path = self.filename path = make_iterable(path) for filename in path: filename = os.path.expanduser(filename) if os.path.exists(filename): self.filename = filename return self.read(filename) self._add_default_connection() def read(self, filename): """Reads the file specified by filename This method will load the eapi.conf file specified by filename into the instance object. It will also add the default connection localhost if it was not defined in the eapi.conf file Args: filename (str): The full path to the file to load """ try: SafeConfigParser.read(self, filename) except SafeConfigParserError as exc: # Ignore file and syslog a message on SafeConfigParser errors msg = ("%s: parsing error in eapi conf file: %s" % (type(exc).__name__, filename)) debug(msg) self._add_default_connection() for name in self.sections(): if name.startswith('connection:') and \ 'host' not in dict(self.items(name)): self.set(name, 'host', name.split(':')[1]) self.generate_tags() def _add_default_connection(self): """Checks the loaded config and adds the localhost profile if needed This method wil load the connection:localhost profile into the client configuration if it is not already present. """ if not self.get_connection('localhost'): self.add_connection('localhost', transport='socket') def generate_tags(self): """ Generates the tags with collection with hosts """ self.tags = dict() for section in self.sections(): if self.has_option(section, 'tags'): tags = self.get(section, 'tags') for tag in [str(t).strip() for t in tags.split(',')]: if tag not in self.tags: self.tags[tag] = list() self.tags[tag].append(section.split(':')[1]) def load(self, filename): """Loads the file specified by filename This method works in conjunction with the autoload method to load the file specified by filename. Args: filename (str): The full path to the file to be loaded """ self.filename = filename self.reload() def reload(self): """Reloades the configuration This method will reload the configuration instance using the last known filename. Note this method will initially clear the configuration and reload all entries. """ for section in self.sections(): self.remove_section(section) self.autoload() def get_connection(self, name): """Returns the properties for a connection name This method will return the settings for the configuration specified by name. Note that the name argument should only be the name. For instance, give the following eapi.conf file .. code-block:: ini [connection:veos01] transport: http The name to use to retrieve the configuration would be veos01 >>> pyeapi.client.config.get_connection('veos01') Args: name (str): The name of the connection to return Returns: A Python dictionary object of key/value pairs that represent the node configuration. If the name provided in the argument is not found, then None is returned. """ name = 'connection:{}'.format(name) if not self.has_section(name): return None return dict(self.items(name)) def add_connection(self, name, **kwargs): """Adds a connection to the configuration This method will add a connection to the configuration. The connection added is only available for the lifetime of the object and is not persisted. Note: If a call is made to load() or reload(), any connections added with this method must be re-added to the config instance Args: name (str): The name of the connection to add to the config. The name provided will automatically be prepended with the string connection: **kwargs (dict); The set of properties used to provide the node configuration """ name = 'connection:{}'.format(name) self.add_section(name) for key, value in list(kwargs.items()): self.set(name, key, value) self.generate_tags() # TODO: This is a global variable (in the module) - to review the impact on # having a shared state for the config file. config = Config() def load_config(filename): """Function method that loads a conf file This function will load the file specified by filename into the config instance. Its a convenience function that calls load on the config instance Args: filename (str): The full path to the filename to load """ return config.load(filename) def config_for(name): """ Function to get settings for named config This function will return the settings for a specific connection as specified by name. Its a convenience function that calls get_connection on the global config instance Args: name (str): The name of the connection to return. The connection name is specified as the string right of the : in the INI file Returns: A Python dictionary object of key/value pairs that represent the nodes configuration settings from the config instance """ return config.get_connection(name) def hosts_for_tag(tag): """ Returns the hosts assocated with the specified tag This function will return the hosts assoicated with the tag specified in the argument. It will return an array of connecition names. Args: tag (str): The name of the tag to retrieve the list of hosts for Returns: list: A Python list object that includes the list of hosts assoicated with the specified tag. None: If the specified tag does not exist, then None is returned. """ return config.tags.get(tag) def make_connection(transport, **kwargs): """ Creates a connection instance based on the transport This function creates the EapiConnection object based on the desired transport. It looks up the transport class in the TRANSPORTS global dictionary. Args: transport (string): The transport to use to create the instance. **kwargs: Arbitrary keyword arguments. Returns: An instance of a connection object based on the transport Raises: TypeError: A TypeError is raised if the transport keyword is not found in the list (keys) of available transports. """ if transport not in TRANSPORTS: raise TypeError('invalid transport specified') klass = TRANSPORTS[transport] return klass(**kwargs) def connect(transport=None, host='localhost', username='admin', password='', port=None, key_file=None, cert_file=None, ca_file=None, timeout=60, return_node=False, context=None, **kwargs): """ Creates a connection using the supplied settings This function will create a connection to an Arista EOS node using the arguments. All arguments are optional with default values. Args: transport (str): Specifies the type of connection transport to use. Valid values for the connection are socket, http_local, http, https, https_certs, http_session, and https_session. The default value is specified in DEFAULT_TRANSPORT host (str): The IP addres or DNS host name of the connection device. The default value is 'localhost' username (str): The username to pass to the device to authenticate the eAPI connection. The default value is 'admin' password (str): The password to pass to the device to authenticate the eAPI connection. The default value is '' port (int): The TCP port of the endpoint for the eAPI connection. If this keyword is not specified, the default value is automatically determined by the transport type. (http=80, https=443) key_file (str): Path to private key file for ssl validation cert_file (str): Path to PEM formatted cert file for ssl validation ca_file (str): Path to CA PEM formatted cert file for ssl validation timeout (int): timeout context (ssl.SSLContext): ssl object's context. The default is None return_node (bool): Returns a Node object if True, otherwise returns an EapiConnection object. Returns: An instance of an EapiConnection object for the specified transport. Note: Python 3.10 increases security strength of the TLS stack by among other things using a stronger (than 3.9) default cipher suite. Thus programs relying on the https transport and using the default cypher suite that used to work in prior python versions may fail with the error: ``[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] sslv3 alert handshake failure``. The solution to that issue is to configure the https web server to use a stronger cipher suite. If the solution is not attainable, then a work-around might be considered (weighing all due implications) - one could pass an ssl context where cipher can be specified:: import pyeapi import ssl ... ctx = ssl.create_default_context() ctx.set_ciphers('DEFAULT') # set a preferred cipher ctx.check_hostname = False # for the sake of example ctx.verify_mode = ssl.CERT_NONE # do it w/o certificate ... cc = pyeapi.client.connect( host=host_name, context=ctx ) """ transport = transport or DEFAULT_TRANSPORT connection = make_connection(transport, host=host, username=username, password=password, key_file=key_file, cert_file=cert_file, ca_file=ca_file, port=port, timeout=timeout, context=context) if return_node: return Node(connection, transport=transport, host=host, username=username, password=password, key_file=key_file, cert_file=cert_file, ca_file=ca_file, port=port, **kwargs) return connection class Node(object): """Represents a single device for sending and receiving eAPI messages The Node object provides an instance for communicating with Arista EOS devices. The Node object provides easy to use methods for sending both enable and config commands to the device using a specific transport. This object forms the base for communicating with devices. Attributes: connection (EapiConnection): The connection property represents the underlying transport used by the Node object to communicate with the device using eAPI. running_config (str): The running-config from the device. This property is lazily loaded and refreshed over the life cycle of the instance. startup_config (str): The startup-config from the device. This property is lazily loaded and refreshed over the life cycle of the instance. autorefresh (bool): If True, the running-config and startup-config are refreshed on config events. If False, then the config properties must be manually refreshed. config_defaults (bool): If True, the default config options will be shown in the running-config output settings (dict): Provides access to the settings used to create the Node instance. Args: connection (EapiConnection): An instance of EapiConnection used as the transport for sending and receiving eAPI requests and responses. **kwargs: An arbitrary list of keyword arguments """ def __init__(self, connection, **kwargs): self._connection = connection self._running_config = None self._startup_config = None self._version = None self._version_number = None self._model = None self._session_name = None self._enablepwd = kwargs.get('enablepwd') self.autorefresh = kwargs.get('autorefresh', True) self.config_defaults = kwargs.get('config_defaults', True) self.settings = kwargs def __str__(self): return 'Node(connection=%s)' % str(self._connection) def __repr__(self): return 'Node(connection=%s)' % repr(self._connection) @property def connection(self): return self._connection @property def running_config(self): if self._running_config is not None: return self._running_config params = 'all' if self.config_defaults else None self._running_config = self.get_config(params=params, as_string=True) return self._running_config @property def startup_config(self): if self._startup_config is not None: return self._startup_config self._startup_config = self.get_config('startup-config', as_string=True) return self._startup_config @property def version(self): if self._version: return self._version self._get_version_properties() return self._version @property def version_number(self): if self._version_number: return self._version_number self._get_version_properties() return self._version_number @property def model(self): if self._model: return self._model self._get_version_properties() return self._model def _get_version_properties(self): """Parses version and model information out of 'show version' output and uses the output to populate class properties. """ # Parse out version info output = self.enable('show version') self._version = str(output[0]['result']['version']) match = re.match(r'[\d.\d]+', str(output[0]['result']['version'])) if match: self._version_number = str(match.group(0)) else: self._version_number = str(output[0]['result']['version']) # Parse out model number match = re.search(r'\d\d\d\d', str(output[0]['result']['modelName'])) if match: self._model = str(match.group(0)) else: self._model = str(output[0]['result']['modelName']) def enable_authentication(self, password): """Configures the enable mode authentication password EOS supports an additional password authentication mechanism for sessions that want to switch to executive (or enable) mode. This method will configure the password, if required, for entering executive mode Args: password (str): The password string in clear text used to authenticate to exec mode """ self._enablepwd = str(password).strip() def config(self, commands, **kwargs): """Configures the node with the specified commands This method is used to send configuration commands to the node. It will take either a string, list or CliVariants type and prepend the necessary commands to put the session into config mode. pyeapi.utils.CliVariants facilitates alternative executions to commands sequence until one variant succeeds or all fail Args: commands (str, list, CliVariants): The commands to send to the node in config mode. If the commands argument is an str or CliVariants type, it will be cast to a list. The list of commands will also be prepended with the necessary commands to put the session in config mode. CliVariants could be part of a list too, however only a single occurrence of CliVariants type in commands is supported. CliVariants type facilitates execution of alternative commands sequences, e.g.: ``config( [cli1, CliVariants( cli2, cli3 ), cli4] )`` the example above can be translated into following sequence: ``config( [cli1, cli2, cli4] )`` ``config( [cli1, cli3, cli4] )`` CliVariants accepts 2 or more arguments of str, list type, or their mix. Each argument to CliVariants will be joined with the rest of commands and all command sequences will be tried until one variant succeeds. If all variants fail the last failure exception will be re-raised. **kwargs: Additional keyword arguments for expanded eAPI functionality. Only supported eAPI params are used in building the request Returns: The config method will return a list of dictionaries with the output from each command. The function will strip the response from any commands it prepends. """ def variant_cli_idx( cmds ): # return index of first occurrence of CliVariants type in cmds try: return [ type(v) for v in cmds ].index( CliVariants ) except (ValueError): return -1 cfg_call = self._configure_session if self._session_name \ else self._configure_terminal if isinstance( commands, CliVariants ): commands = [ commands ] idx = variant_cli_idx( commands ) if idx == -1: return cfg_call( commands, **kwargs ) # commands contain CliVariants obj, e.g.: [ '...', CliVariants, ... ] err = None for variant in commands[ idx ].variants: cmd = commands[ :idx ] + variant + commands[ idx + 1: ] try: return cfg_call( cmd, **kwargs ) except (CommandError) as exp: err = exp raise err # re-raising last occurred CommandError def _configure_terminal(self, commands, **kwargs): """Configures the node with the specified commands with leading "configure terminal" """ commands = make_iterable(commands) commands = list(commands) # push the configure command onto the command stack commands.insert(0, 'configure terminal') response = self.run_commands(commands, **kwargs) # after config change the _chunkify lru_cache has to be cleared self._chunkify.cache_clear() if self.autorefresh: self.refresh() # pop the configure command output off the stack response.pop(0) return response def _configure_session(self, commands, **kwargs): """Configures the node with the specified commands with leading "configure session " """ if not self._session_name: raise CommandError(-1, 'Not currently in a session') commands = make_iterable(commands) commands = list(commands) # push the configure command onto the command stack commands.insert(0, 'configure session %s' % self._session_name) response = self.run_commands(commands, **kwargs) # after config change the _chunkify lru_cache has to be cleared self._chunkify.cache_clear() # pop the configure command output off the stack response.pop(0) return response @lru_cache(maxsize=None) def _chunkify( self, config, indent=0 ): """parse device config and return a dict holding sections and sub-sections: - a section always begins with a line with zero indents, - a sub-section always begins with an indented line a (sub)section typically contains a begin line (with a lower indent) and a body (with a higher indent). A section might be degenerative (no body, just the section line itself), while sub-sections always contain a sub-section line plus some body). E.g., here's a snippet of a section dict: { ... 'spanning-tree mode none': 'spanning-tree mode none\n', ... 'mac security': 'mac security\n profile PR\n cipher aes256-gcm', ' profile PR': ' profile PR\n cipher aes256-gcm' ... } it's imperative that the most outer call is made with indent=0, as the indent parameter defines processing of nested sub-sections, i.e., if indent > 0, then it's a recursive call and `config` argument contains last parsed (sub)section, which in turn may contain sub-sections """ def is_subsection_present( section, indent ): return any( [line[ indent ] == ' ' for line in section] ) sections = {} key = None for line in config.splitlines( keepends=True )[ indent > 0: ]: # indent > 0: no need processing subsection line, which is 1st line if line[ indent ] == ' ': # section continuation sections[key] += line continue # new section is found (if key is not None) if key: # process prior (last recorded) section lines = sections[key].splitlines()[ 1: ] if len( lines ): # section may contain sub-sections ind = len( lines[0] ) - len( lines[0].lstrip() ) if is_subsection_present( lines, ind ): subs = self._chunkify( sections[key], indent=ind ) subs.update( sections ) sections = subs elif indent > 0: # record only subsections del sections[key] key = line.rstrip() sections[key] = line return sections def section(self, regex, config='running_config'): """Returns a section of the config Args: regex (str): A valid regular expression used to select sections of configuration to return config (str): The configuration to return. Valid values for config are "running_config" or "startup_config". The default value is "running_config" Returns: The configuration section as a string object. """ if config in ['running_config', 'startup_config']: config = getattr(self, config) chunked = self._chunkify(config) r = re.compile(regex) matching_keys = [k for k in chunked.keys() if r.search(k)] if len(matching_keys) == 0: raise TypeError('config section not found') matching_key = matching_keys[0] match = chunked[matching_key] return match def enable(self, commands, encoding='json', strict=False, send_enable=True, **kwargs): """Sends the array of commands to the node in enable mode This method will send the commands to the node and evaluate the results. If a command fails due to an encoding error, then the command set will be re-issued individual with text encoding. Args: commands (list): The list of commands to send to the node encoding (str): The requested encoding of the command output. Valid values for encoding are JSON or text strict (bool): If False, this method will attempt to run a command with text encoding if JSON encoding fails send_enable (bool): If True the enable command will be prepended to the command list automatically. **kwargs: Additional keyword arguments for expanded eAPI functionality. Only supported eAPI params are used in building the request Returns: A dict object that includes the response for each command along with the encoding Raises: TypeError: This method does not support sending configure commands and will raise a TypeError if configuration commands are found in the list of commands provided This method will also raise a TypeError if the specified encoding is not one of 'json' or 'text' CommandError: This method will raise a CommandError if any one of the commands fails. """ commands = make_iterable(commands) if 'configure' in commands: raise TypeError('config mode commands not supported') results = list() # IMPORTANT: There are two keys (response, result) that both # return the same value. 'response' was originally placed # there in error and both are now present to avoid breaking # existing scripts. 'response' will be removed in a future release. if strict: responses = self.run_commands(commands, encoding, send_enable, **kwargs) for index, response in enumerate(responses): results.append(dict(command=commands[index], result=response, response=response, encoding=encoding)) else: for command in commands: try: resp = self.run_commands(command, encoding, send_enable, **kwargs) results.append(dict(command=command, result=resp[0], encoding=encoding)) except CommandError as exc: if exc.error_code == 1003: resp = self.run_commands(command, 'text', send_enable, **kwargs) results.append(dict(command=command, result=resp[0], encoding='text')) else: raise return results def run_commands(self, commands, encoding='json', send_enable=True, **kwargs): """Sends the commands over the transport to the device This method sends the commands to the device using the nodes transport. This is a lower layer function that shouldn't normally need to be used, preferring instead to use config() or enable(). Args: commands (list): The ordered list of commands to send to the device using the transport encoding (str): The encoding method to use for the request and excpected response. send_enable (bool): If True the enable command will be prepended to the command list automatically. **kwargs: Additional keyword arguments for expanded eAPI functionality. Only supported eAPI params are used in building the request Returns: This method will return the raw response from the connection which is a Python dictionary object. """ commands = make_iterable(commands) # Some commands are multiline commands. These are banner commands and # SSL commands. So with this two lines we # can support those by passing commands by doing: # banner login MULTILINE: This is my banner.\nAnd I even support # multiple lines. # Why this? To be able to read a configuration from a file, split it # into lines and pass it as it is # to pyeapi without caring about multiline commands. commands = [{'cmd': c.split('MULTILINE:')[0], 'input': '%s\n' % (c.split('MULTILINE:')[1].strip())} if 'MULTILINE:' in c else c for c in commands] if send_enable: if self._enablepwd: commands.insert(0, {'cmd': 'enable', 'input': self._enablepwd}) else: commands.insert(0, 'enable') response = self._connection.execute(commands, encoding, **kwargs) # pop enable command from the response only if we sent enable if send_enable: response['result'].pop(0) return response['result'] def api(self, name, namespace='pyeapi.api'): """Loads the specified api module This method is the API autoload mechanism that will load the API module specified by the name argument. The API module will be loaded and look first for an initialize() function and secondly for an instance() function. In both cases, the node object is passed to the module. Args: name (str): The name of the module to load. The name should be the name of the python file to import namespace (str): The namespace to use to load the module. The default value is 'pyeapi.api' Returns: The API module loaded with the node instance. """ module = load_module('{}.{}'.format(namespace, name)) if hasattr(module, 'initialize'): module.initialize(self) if hasattr(module, 'instance'): return module.instance(self) return module def get_config(self, config='running-config', params=None, as_string=False): """ Retreives the config from the node This method will retrieve the config from the node as either a string or a list object. The config to retrieve can be specified as either the startup-config or the running-config. Args: config (str): Specifies to return either the nodes startup-config or running-config. The default value is the running-config params (str): A string of keywords to append to the command for retrieving the config. as_string (boo): Flag that determines the response. If True, then the configuration is returned as a raw string. If False, then the configuration is returned as a list. The default value is False Returns: This method will return either a string or a list depending on the states of the as_string keyword argument. Raises: TypeError: If the specified config is not one of either 'running-config' or 'startup-config' """ if config not in ['startup-config', 'running-config']: raise TypeError('invalid config name specified') command = 'show %s' % config if params: command += ' %s' % params result = self.run_commands(command, 'text') if as_string: return str(result[0]['output']).strip() return str(result[0]['output']).split('\n') def refresh(self): """Refreshes the instance config properties This method will refresh the public running_config and startup_config properites. Since the properties are lazily loaded, this method will clear the current internal instance variables. One the next call the instance variables will be repopulated with the current config """ self._running_config = None self._startup_config = None def configure_session(self): """Enter a config session """ self._session_name = self._session_name or uuid4() def diff(self): """Returns session-config diffs in text encoding Note: "show session-config diffs" doesn't support json encoding """ response = self._configure_session( ['show session-config diffs'], encoding='text' ) return response[0]['output'] def commit(self): """Commits the current config session """ return self._configure_and_exit_session(['commit']) def abort(self): """Aborts the current config session """ return self._configure_session(['abort']) def _configure_and_exit_session(self, commands, **kwargs): response = self._configure_session(commands, **kwargs) if self.autorefresh: self.refresh() # Exit the current config session self._session_name = None return response def connect_to(name): """Creates a node instance based on an entry from the config This function will retrieve the settings for the specified connection from the config and return a Node instance. The configuration must be loaded prior to calling this function. Args: name (str): The name of the connection to load from the config. The name argument should be the connection name (everything right of the colon from the INI file) Returns: This function will return an instance of Node with the settings from the config instance. Raises: AttributeError: raised if the specified configuration name is not found in the loaded configuration """ kwargs = config_for(name) if not kwargs: raise AttributeError('connection profile not found in config') node = connect(return_node=True, **kwargs) return node pyeapi-1.0.2/pyeapi/eapilib.py0000644000076500000240000006760414447405743017436 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # """Provides wrapper for eAPI calls This module provides a connection to eAPI by wrapping eAPI calls in an instance of Connection. The connection module provides an easy implementation for sending and receiving calls over eAPI using a HTTP/S transport. """ import socket import base64 import logging import ssl import re try: import ujson as json except ImportError: try: import rapidjson as json except ImportError: import json from http.client import HTTPConnection, HTTPSConnection from http.cookies import SimpleCookie from pyeapi.utils import make_iterable _LOGGER = logging.getLogger(__name__) DEFAULT_HTTP_PORT = 80 DEFAULT_HTTPS_PORT = 443 DEFAULT_HTTP_LOCAL_PORT = 8080 DEFAULT_HTTPS_LOCAL_PORT = 8443 DEFAULT_HTTP_PATH = '/command-api' DEFAULT_UNIX_SOCKET = '/var/run/command-api.sock' def https_connection_factory(path, host, port, context=None, timeout=60): return HttpsConnection(path, host, port, context=context, timeout=timeout) class EapiError(Exception): """Base exception class for all exceptions generated by eapilib This is the base exception class for all exceptions generated by eapilib. It is provided as a catch all for exceptions and should not be directly raised by an methods or functions Args: commands (array): The list of commands there were sent to the node that when the exception was raised message (string): The exception error message """ def __init__(self, message, commands=None): self.message = message self.commands = commands super(EapiError, self).__init__(message) class CommandError(EapiError): """Base exception raised for command errors The CommandError instance provides a custom exception that can be used if the eAPI command(s) fail. It provides some additional information that can be used to understand what caused the exception. Args: error_code (int): The error code returned from the eAPI call. error_text (string): The error text message that coincides with the error_code commands (array): The list of commands that were sent to the node that generated the error message (string): The exception error message which is a concatenation of the error_code and error_text """ def __init__(self, code, message, **kwargs): cmd_err = kwargs.get('command_error') if int(code) in [1000, 1001, 1002, 1004]: msg_fmt = 'Error [{}]: {} [{}]'.format(code, message, cmd_err) else: # error code 1005: 'Command unauthorized: user has insufficient # permissions to run the command'. The message contains a user # sensitive input, which has to be removed if int(code) == 1005: message = re.sub(r"(?<=input=)[^ ]+", r')', message) msg_fmt = 'Error [{}]: {}'.format(code, message) super(CommandError, self).__init__(msg_fmt) self.error_code = code self.error_text = message self.command_error = cmd_err self.commands = kwargs.get('commands') self.output = kwargs.get('output') self.message = msg_fmt @property def trace(self): return self.get_trace() def get_trace(self): trace = list() index = None for index, out in enumerate(self.output): _entry = {'command': self.commands[index], 'output': out} trace.append(_entry) if index: index += 1 for cmd in self.commands[index:]: _entry = {'command': cmd, 'output': None} trace.append(_entry) return trace class ConnectionError(EapiError): """Base exception raised for connection errors Connection errors are raised when a connection object is unable to connect to the node. Typically these errors can result from using the wrong transport type or not providing valid credentials. Args: commands (array): The list of commands there were sent to the node that when the exception was raised connection_type (string): The string identifier for the connection object that generate the error message (string): The exception error message response (string): The message generate from the response packet """ def __init__(self, connection_type, message, commands=None): self.message = message self.connection_type = connection_type self.commands = commands super(ConnectionError, self).__init__(message) class SocketConnection(HTTPConnection): def __init__(self, path, timeout=60): HTTPConnection.__init__(self, 'localhost') self.path = path self.timeout = timeout def __str__(self): return 'unix:%s' % self.path def __repr__(self): return 'unix:%s' % self.path def connect(self): self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock.settimeout(self.timeout) self.sock.connect(self.path) class HttpConnection(HTTPConnection): def __init__(self, path, *args, **kwargs): HTTPConnection.__init__(self, *args, **kwargs) self.path = path def __str__(self): return 'http://%s:%s/%s' % (self.host, self.port, self.path) def __repr__(self): return 'http://%s:%s/%s' % (self.host, self.port, self.path) class HttpsConnection(HTTPSConnection): def __init__(self, path, *args, **kwargs): HTTPSConnection.__init__(self, *args, **kwargs) self.path = path def __str__(self): return 'https://%s:%s/%s' % (self.host, self.port, self.path) def __repr__(self): return 'https://%s:%s/%s' % (self.host, self.port, self.path) class HTTPSCertConnection(HTTPSConnection): """ Class to make a HTTPS connection, with support for full client-based SSL Authentication. """ def __init__(self, path, host, port, key_file, cert_file, ca_file, timeout=None): HTTPSConnection.__init__(self, host, key_file=key_file, cert_file=cert_file) self.key_file = key_file self.cert_file = cert_file self.ca_file = ca_file self.timeout = timeout self.path = path self.port = port def __str__(self): return 'https://%s:%s/%s - %s,%s' % (self.host, self.port, self.path, self.key_file, self.cert_file) def __repr__(self): return 'https://%s:%s/%s - %s,%s' % (self.host, self.port, self.path, self.key_file, self.cert_file) def connect(self): """ Connect to a host on a given (SSL) port. If ca_file is pointing somewhere, use it to check Server Certificate. Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to ssl.wrap_socket() (Now changed to ssl.SSLContext.wrap_socket() as the former has been deprecated from python 3.7), which forces SSL to check server certificate against our client certificate. """ sock = socket.create_connection((self.host, self.port), self.timeout) if self._tunnel_host: self.sock = sock self._tunnel() # If there's no CA File, don't force Server Certificate Check if self.ca_file: self.sock = ssl.SSLContext.wrap_socket(sock, self.key_file, self.cert_file, ca_certs=self.ca_file, cert_reqs=ssl.CERT_REQUIRED) else: self.sock = ssl.SSLContext.wrap_socket(sock, self.key_file, self.cert_file, cert_reqs=ssl.CERT_NONE) class EapiConnection(object): """Creates a connection to eAPI for sending and receiving eAPI requests The EapiConnection object provides an implementation for sending and receiving eAPI requests and responses. This class should not need to be instantiated directly. """ def __init__(self): self.transport = None self.error = None self.socket_error = None self._auth = None def __str__(self): return 'EapiConnection(transport=%s)' % str(self.transport) def __repr__(self): return 'EapiConnection(transport=%s)' % repr(self.transport) def authentication(self, username, password): """Configures the user authentication for eAPI This method configures the username and password combination to use for authenticating to eAPI. Args: username (str): The username to use to authenticate the eAPI connection with password (str): The password in clear text to use to authenticate the eAPI connection with """ _auth_text = '{}:{}'.format(username, password) _auth_bin = base64.encodebytes(_auth_text.encode()) _auth = _auth_bin.decode().replace('\n', '') self._auth = ("Authorization", "Basic %s" % _auth) _LOGGER.debug('Authentication string is: {}:***'.format(username)) def request(self, commands, encoding=None, reqid=None, **kwargs): """Generates an eAPI request object This method will take a list of EOS commands and generate a valid eAPI request object form them. The eAPI request object is then JSON encoding and returned to the caller. eAPI Request Object .. code-block:: json { "jsonrpc": "2.0", "method": "runCmds", "params": { "version": 1, "cmds": [ ], "format": [json, text], } "id": } Args: commands (list): A list of commands to include in the eAPI request object encoding (string): The encoding method passed as the `format` parameter in the eAPI request reqid (string): A custom value to assign to the request ID field. This value is automatically generated if not passed **kwargs: Additional keyword arguments for expanded eAPI functionality. Only supported eAPI params are used in building the request Returns: A JSON encoding request structure that can be send over eAPI """ commands = make_iterable(commands) reqid = id(self) if reqid is None else reqid params = {'version': 1, 'cmds': commands, 'format': encoding} streaming = False if 'autoComplete' in kwargs: params['autoComplete'] = kwargs['autoComplete'] if 'expandAliases' in kwargs: params['expandAliases'] = kwargs['expandAliases'] if 'streaming' in kwargs: streaming = kwargs['streaming'] return json.dumps({'jsonrpc': '2.0', 'method': 'runCmds', 'params': params, 'id': str(reqid), 'streaming': streaming}) def send(self, data): """Sends the eAPI request to the destination node This method is responsible for sending an eAPI request to the destination node and returning a response based on the eAPI response object. eAPI responds to request messages with either a success message or failure message. eAPI Response - success .. code-block:: json { "jsonrpc": "2.0", "result": [ {}, {} { "warnings": [ ] }, ], "id": } eAPI Response - failure .. code-block:: json { "jsonrpc": "2.0", "error": { "code": , "message": "data": [ {}, {}, { "errors": [ ] } ] } "id": } Args: data (string): The data to be included in the body of the eAPI request object Returns: A decoded response. The response object is deserialized from JSON and returned as a standard Python dictionary object Raises: CommandError if an eAPI failure response object is returned from the node. The CommandError exception includes the error code and error message from the eAPI response. """ try: _LOGGER.debug( 'Request content: {}'.format(self._sanitize_request( data )) ) # debug('eapi_request: %s' % data) self.transport.putrequest('POST', '/command-api') self.transport.putheader('Content-type', 'application/json-rpc') self.transport.putheader('Content-length', '%d' % len(data)) if self._auth: self.transport.putheader(*self._auth) data = data.encode() self.transport.endheaders(message_body=data) response = self.transport.getresponse() response_content = response.read() if isinstance( response_content, bytes ): response_content = response_content.decode() _LOGGER.debug('Response: status:{status}, reason:{reason}'.format( status=response.status, reason=response.reason)) _LOGGER.debug('Response content: {}'.format(response_content)) if response.status == 401: raise ConnectionError(str(self), f'{response.reason}. {response_content}') # Python 3.7 json.loads() works with bytes or strings, # thus no decoding is required decoded = json.loads(response_content) _LOGGER.debug('eapi_response: %s' % decoded) if 'error' in decoded: (code, msg, err, out) = self._parse_error_message(decoded) pattern = "unexpected keyword argument '(.*)'" match = re.search(pattern, msg) if match: auto_msg = ('%s parameter is not supported in this' ' version of EOS.' % match.group(1)) _LOGGER.error(auto_msg) msg = msg + '. ' + auto_msg raise CommandError(code, msg, command_error=err, output=out) return decoded # removed socket.error as it's deprecated in python 3 except OSError as exc: _LOGGER.exception(exc) self.socket_error = exc self.error = exc error_msg = 'Socket error during eAPI connection: %s' % str(exc) raise ConnectionError(str(self), error_msg) except ValueError as exc: _LOGGER.exception(exc) self.socket_error = None self.error = exc raise ConnectionError(str(self), 'unable to connect to eAPI') finally: self.transport.close() def _sanitize_request( self, data ): """remove user-sensitive input from data response""" try: data_json = json.loads( data ) match = self._find_sub_json( data_json, {'cmd': 'enable', 'input': ()} ) if match: match.entry[ match.idx ][ 'input' ] = '' return json.dumps( data_json ) except ValueError: pass return data def _find_sub_json( self, jsn, sbj, instance=0 ): """finds a subset (sbj) in json. `sbj` must be a subset and json must not be atomic. Wildcard(s) in `sbj` can be specified with tuple type. A json label cannot be wildcarded. A single wildcard represent a single json entry. E.g.: _find_sub_json( jsn, { 'foo': () } ) Returned value is a Match class with attributes: - entry: an iterable containing a matching `sbj` - idx: index or key pointing to the match in the iterable If no match found None is returned - that way is possible to get a reference to the sought json and modify it, e.g: match = _find_sub_json( jsn, { 'foo':(), 'bar': [123, (), ()] } ) if match: match.entry[ match.idx ][ 'foo' ] = 'bar' It's also possible to specify an occurrence of the match via `instance` parameter - by default a first found match is returned""" class Match(): def __init__( self, entry, idx ): self.entry = entry self.idx = idx def is_iterable( val ): return True if isinstance( val, (list, dict) ) else False def is_atomic( val ): return not is_iterable( val ) def is_match( jsn, sbj ): if isinstance( sbj, tuple ): # sbj is a wildcard return True if is_atomic( sbj ): return False if is_iterable( jsn ) else sbj == jsn if type( jsn ) is not type( sbj ) or len( jsn ) != len( sbj ): return False for left, right in zip( sorted(jsn.items() if isinstance( jsn, dict ) else enumerate( jsn )), sorted(sbj.items() if isinstance( sbj, dict ) else enumerate( sbj )) ): if left[ 0 ] != right[ 0 ]: return False if not is_match( left[ 1 ], right[ 1 ]): return False return True if is_atomic( jsn ): return None instance = [ instance ] if isinstance( instance, int ) else instance for key, val in jsn.items() if isinstance( jsn, dict ) else enumerate( jsn ): if is_match( val, sbj ): if instance[ 0 ] > 0: instance[ 0 ] -= 1 else: return Match( jsn, key ) if is_iterable( val ): match = self._find_sub_json( val, sbj, instance ) if match: return match return None def _parse_error_message(self, message): """Parses the eAPI failure response message This method accepts an eAPI failure message and parses the necesary parts in order to generate a CommandError. Args: message (str): The error message to parse Returns: tuple: A tuple that consists of the following: * code: The error code specified in the failure message * message: The error text specified in the failure message * error: The error text from the command that generated the error (the last command that ran) * output: A list of all output from all commands """ msg = message['error']['message'] code = message['error']['code'] err = None out = None if 'data' in message['error']: err = [] for dct in message['error']['data']: err.extend( ['%s: %s' % ( k, repr(v) ) for k, v in dct.items()] ) err = ', '.join(err) out = message['error']['data'] return code, msg, err, out def execute(self, commands, encoding='json', **kwargs): """Executes the list of commands on the destination node This method takes a list of commands and sends them to the destination node, returning the results. The execute method handles putting the destination node in enable mode and will pass the enable password, if required. Args: commands (list): A list of commands to execute on the remote node encoding (string): The encoding to send along with the request message to the destination node. Valid values include 'json' or 'text'. This argument will influence the response object encoding **kwargs: Arbitrary keyword arguments Returns: A decoded response message as a native Python dictionary object that has been deserialized from JSON. Raises: CommandError: A CommandError is raised that includes the error code, error message along with the list of commands that were sent to the node. The exception instance is also stored in the error property and is availble until the next request is sent """ if encoding not in ('json', 'text'): raise TypeError('encoding must be one of [json, text]') try: self.error = None request = self.request(commands, encoding=encoding, **kwargs) response = self.send(request) return response except (ConnectionError, CommandError, TypeError) as exc: exc.commands = commands self.error = exc raise class SocketEapiConnection(EapiConnection): def __init__(self, path=None, timeout=60, **kwargs): super(SocketEapiConnection, self).__init__() path = path or DEFAULT_UNIX_SOCKET self.transport = SocketConnection(path, timeout) class HttpLocalEapiConnection(EapiConnection): def __init__(self, port=None, path=None, timeout=60, **kwargs): super(HttpLocalEapiConnection, self).__init__() port = port or DEFAULT_HTTP_LOCAL_PORT path = path or DEFAULT_HTTP_PATH self.transport = HttpConnection(path, 'localhost', int(port), timeout=timeout) class HttpEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, password=None, timeout=60, **kwargs): super(HttpEapiConnection, self).__init__() port = port or DEFAULT_HTTP_PORT path = path or DEFAULT_HTTP_PATH self.transport = HttpConnection(path, host, int(port), timeout=timeout) self.authentication(username, password) class HttpsEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, password=None, context=None, timeout=60, **kwargs): super(HttpsEapiConnection, self).__init__() port = port or DEFAULT_HTTPS_PORT path = path or DEFAULT_HTTP_PATH enforce_verification = kwargs.get('enforce_verification') # after fix #236 (allowing passing ssl context), this parameter # is deprecated - will be release noted and removed in the respective # release versions if context is None and not enforce_verification: context = self.disable_certificate_verification() self.transport = https_connection_factory(path, host, int(port), context, timeout) self.authentication(username, password) def disable_certificate_verification(self): # SSL/TLS certificate verification is enabled by default in latest # Python releases and causes self-signed certificates generated # on EOS to fail validation (unless explicitly imported). # Disable the SSL/TLS certificate verification for now. # Use the approach in PEP476 to disable certificate validation. # TODO: # ************************** WARNING ***************************** # This behaviour is considered a *security risk*, so use it # temporary until a proper fix is implemented. if hasattr(ssl, '_create_unverified_context'): return ssl._create_unverified_context() class HttpsEapiCertConnection(EapiConnection): def __init__(self, host, port=None, path=None, key_file=None, cert_file=None, ca_file=None, timeout=60, **kwargs): if key_file is None or cert_file is None: raise ValueError("For https_cert connections both a key_file and " "cert_file are required. A ca_file is also " "recommended") super(HttpsEapiCertConnection, self).__init__() port = port or DEFAULT_HTTPS_PORT path = path or DEFAULT_HTTP_PATH self.transport = HTTPSCertConnection(path, host, int(port), key_file=key_file, cert_file=cert_file, ca_file=ca_file, timeout=timeout) class SessionApiConnection(object): def authentication(self, username, password): try: data = json.dumps({"username": username, "password": password}) self.transport.putrequest("POST", "/login") self.transport.putheader("Content-type", "application/json") self.transport.putheader("Content-length", "%d" % len(data)) data = data.encode() self.transport.endheaders(message_body=data) resp = self.transport.getresponse() if resp.status != 200: raise ConnectionError(str(self), '%s. %s' % (resp.reason, resp.read())) session = SimpleCookie(resp.getheader("Set-Cookie")) self._auth = ("Cookie", session.output(header="", attrs=[])) except OSError as exc: _LOGGER.exception(exc) self.socket_error = exc self.error = exc error_msg = f'Socket error during eAPI authentication: {exc}' raise ConnectionError(str(self), error_msg) except ValueError as exc: _LOGGER.exception(exc) self.socket_error = None self.error = exc raise ConnectionError(str(self), 'unable to connect to eAPI') finally: self.transport.close() class HttpEapiSessionConnection(SessionApiConnection, HttpEapiConnection): pass class HttpsEapiSessionConnection(SessionApiConnection, HttpsEapiConnection): pass pyeapi-1.0.2/pyeapi/utils.py0000644000076500000240000002304114447406053017147 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import sys import importlib import inspect import logging import logging.handlers from collections.abc import Iterable from itertools import tee, zip_longest _LOGGER = logging.getLogger(__name__) _LOGGER.setLevel(logging.DEBUG) # Create a handler to log messages to syslog if sys.platform == "darwin": _syslog_handler = logging.handlers.SysLogHandler(address='/var/run/syslog') else: _syslog_handler = logging.handlers.SysLogHandler() _LOGGER.addHandler(_syslog_handler) # Create a handler to log messages to stderr _stderr_formatter = logging.Formatter('\n\n**** LOG NOTE ****\n%(message)s\n') _stderr_handler = logging.StreamHandler() _stderr_handler.setFormatter(_stderr_formatter) _LOGGER.addHandler(_stderr_handler) def import_module(name): """ Imports a module into the current runtime environment This function emulates the Python import system that allows for importing full path modules. It will break down the module and import each part (or skip if it is already loaded in cache). Args: name (str): The name of the module to import. This should be the full path of the module Returns: The module that was imported """ if name in sys.modules: # Be sure not to reload a previously loaded module mod = sys.modules[name] else: mod = importlib.import_module(name) return mod def load_module(name): """ Attempts to load a module into the current environment This function will load a module specified by name. The module name is first checked to see if it is already loaded and will return the module if it is. If the module hasn't been previously loaded it will attempt to import it Args: name (str): Specifies the full name of the module. For instance pyeapi.api.vlans Returns: The module that has been imported or retrieved from the sys modules """ try: mod = None mod = sys.modules[name] except KeyError: mod = import_module(name) finally: if not mod: raise ImportError('unable to import module %s' % name) return mod class ProxyCall(object): def __init__(self, proxy, method): self.proxy = proxy self.method = method def __call__(self, *args, **kwargs): return self.proxy(self.method, *args, **kwargs) def islocalconnection(): """ Checks if running locally on EOS device or remotely This function will return a boolean indicating if the current execution environment is running locally on an EOS device (True) or running remotely and communicating over HTTP/S (False) Returns: A boolean value that indicates whether or not the current thread is local or remote """ return os.path.exists('/etc/Eos-release') def debug(text): """Log a message to syslog and stderr Args: text (str): The string object to print """ frame = inspect.currentframe().f_back module = frame.f_globals['__name__'] func = frame.f_code.co_name msg = "%s.%s: %s" % (module, func, text) _LOGGER.debug(msg) def make_iterable(value): """Converts the supplied value to a list object This function will inspect the supplied value and return an iterable in the form of a list. Args: value (object): A valid Python object Returns: An iterable object of type list """ if isinstance(value, str) or isinstance( value, dict) or isinstance(value, CliVariants): value = [value] if not isinstance(value, Iterable): raise TypeError('value must be an iterable object') return value def lookahead(it): it1, it2 = tee(iter(it)) next(it2) return zip_longest(it1, it2) def expand_range(arg, value_delimiter=',', range_delimiter='-'): """ Expands a delimited string of ranged integers into a list of strings :param arg: The string range to expand :param value_delimiter: The delimiter that separates values :param range_delimiter: The delimiter that signifies a range of values :return: An array of expanded string values :rtype: list """ values = list() expanded = arg.split(value_delimiter) for item in expanded: if range_delimiter in item: start, end = item.split(range_delimiter) _expand = range(int(start), int(end) + 1) values.extend([str(x) for x in _expand]) else: values.extend([item]) return [str(x) for x in values] def collapse_range(arg, value_delimiter=',', range_delimiter='-'): """ Collapses a list of values into a range set :param arg: The list of values to collapse :param value_delimiter: The delimiter that separates values :param range_delimiter: The delimiter that separates a value range :return: An array of collapsed string values :rtype: list """ values = list() expanded = arg.split(value_delimiter) range_start = None for v1, v2 in lookahead(expanded): if v2: v1 = int(v1) v2 = int(v2) if (v1 + 1) == v2: if not range_start: range_start = v1 elif range_start: item = '{}{}{}'.format(range_start, range_delimiter, v1) values.extend([item]) range_start = None else: values.extend([v1]) elif range_start: item = '{}{}{}'.format(range_start, range_delimiter, v1) values.extend([item]) range_start = None else: values.extend([v1]) return [str(x) for x in values] class CliVariants: """ Provides an interface for cli variants (typically to handle a transition period for a deprecated cli) Instance must be initialized either with 2 or more str variants: ``CliVariants( 'new cli', 'legacy cli' )``, or with 2 or more sequences of cli (or a mix of list and str types), e.g.: ``CliVariants( ['new cli1', 'new cli2'], 'alt cli3', 'legacy cli4' )`` """ @staticmethod def expand( cmds ): """ Expands cmds argument into a list of all CLI variants The method returns a list of all full variant combinations present in the the cmds arguement Args: cmds (list): a list made of str and CliVariants types Returns: expanded list, e.g.: expand( 'x', CliVariants( 'a', 'b'), 'y' ) will return: [ ['x', 'a', 'y'], ['x', 'b', 'y'] ] """ assert isinstance(cmds, list), 'argument cmds must be list type' if not cmds: return [ [] ] head = cmds[0] tail = cmds[1:] if isinstance( head, CliVariants ): return [ v + e for v in head.variants for e in CliVariants.expand( tail ) ] else: return [ [head] + e for e in CliVariants.expand(tail) ] def __init__(self, *cli): assert len( cli ) >= 2, 'must be initialized with 2 or more arguments' self.variants = [ v if not isinstance(v, str) and isinstance(v, Iterable) else [v] for v in cli ] def _interpolate_docstr( *tkns ): """Docstring decorator. SYNOPSIS: MIN_MTU=68 MAX_MTU=65535 @_interpolate_docstr( 'MIN_MTU', 'MAX_MTU' ) def mtu_check( val ): "check mtu against its min value (MIN_MTU) and max value (MAX_MTU)" ... print( mtu_check.__doc__ ) check mtu against its min value (68) and max value (65535) """ def docstr_decorator( user_fn ): """update user_fn_wrapper doc string with the interpolated user_fn's """ def user_fn_wrapper( *args, **kwargs ): return user_fn( *args, **kwargs ) module = sys.modules[ user_fn.__module__ ] docstr = user_fn.__doc__ for tkn in tkns: sval = str( getattr(module, tkn) ) docstr = docstr.replace( tkn, sval ) user_fn_wrapper.__doc__ = docstr return user_fn_wrapper return docstr_decorator pyeapi-1.0.2/pyeapi.egg-info/0000755000076500000240000000000014447406213017125 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/pyeapi.egg-info/PKG-INFO0000644000076500000240000000306414447406213020225 0ustar dlyssenkostaff00000000000000Metadata-Version: 2.1 Name: pyeapi Version: 1.0.2 Summary: Python Client for eAPI Home-page: https://github.com/arista-eosplus/pyeapi Author: Arista EOS+ CS Author-email: eosplus-dev@arista.com License: BSD-3 Description: The Python Client for eAPI ========================== The Python Client for eAPI (pyeapi) is a native Python library wrapper around Arista EOS eAPI. It provides a set of Python language bindings for configuring Arista EOS nodes. The Python library can be used to communicate with EOS either locally (on-box) or remotely (off-box). It uses a standard INI-style configuration file to specify one or more nodes and connection profiles. The pyeapi library also provides an API layer for building native Python objects to interact with the destination nodes. The API layer is a convenient implementation for working with the EOS configuration and is extensible for developing custom implementations. This library is freely provided to the open source community for building robust applications using Arista EOS. Support is provided as best effort through Github issues. Keywords: networking arista eos eapi Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: System Administrators Classifier: Topic :: System :: Networking Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 3 :: Only Provides-Extra: dev Provides-Extra: test pyeapi-1.0.2/pyeapi.egg-info/SOURCES.txt0000644000076500000240000001044114447406213021011 0ustar dlyssenkostaff00000000000000.coveragerc CHANGELOG.md LICENSE MANIFEST.in Makefile README.md VERSION dev-requirements.txt requirements.txt setup.py docs/Makefile docs/conf.py docs/configfile.rst docs/configsessions.rst docs/contribute.rst docs/description.rst docs/examples.rst docs/generate_modules.py docs/index.rst docs/install.rst docs/license.rst docs/modules.rst docs/quickstart.rst docs/release-notes-0.1.0.rst docs/release-notes-0.1.1.rst docs/release-notes-0.2.0.rst docs/release-notes-0.2.1.rst docs/release-notes-0.2.2.rst docs/release-notes-0.2.3.rst docs/release-notes-0.2.4.rst docs/release-notes-0.3.0.rst docs/release-notes-0.3.1.rst docs/release-notes-0.3.2.rst docs/release-notes-0.3.3.rst docs/release-notes-0.4.0.rst docs/release-notes-0.5.0.rst docs/release-notes-0.6.0.rst docs/release-notes-0.6.1.rst docs/release-notes-0.7.0.rst docs/release-notes-0.8.0.rst docs/release-notes-0.8.1.rst docs/release-notes-0.8.2.rst docs/release-notes-0.8.3.rst docs/release-notes-0.8.4.rst docs/release-notes-1.0.0.rst docs/release-notes-1.0.2.rst docs/release-notes.rst docs/requirements.rst docs/subinterfaces.rst docs/support.rst docs/api_modules/_list_of_modules.rst docs/api_modules/abstract.rst docs/api_modules/acl.rst docs/api_modules/bgp.rst docs/api_modules/interfaces.rst docs/api_modules/ipinterfaces.rst docs/api_modules/mlag.rst docs/api_modules/ntp.rst docs/api_modules/ospf.rst docs/api_modules/routemaps.rst docs/api_modules/staticroute.rst docs/api_modules/stp.rst docs/api_modules/switchports.rst docs/api_modules/system.rst docs/api_modules/users.rst docs/api_modules/varp.rst docs/api_modules/vlans.rst docs/api_modules/vrfs.rst docs/api_modules/vrrp.rst docs/client_modules/_list_of_modules.rst docs/client_modules/client.rst docs/client_modules/eapilib.rst docs/client_modules/utils.rst examples/get_config.py examples/nodes.conf examples/simple.py examples/sysmac.py examples/vlans_api.py pyeapi/__init__.py pyeapi/client.py pyeapi/eapilib.py pyeapi/utils.py pyeapi.egg-info/PKG-INFO pyeapi.egg-info/SOURCES.txt pyeapi.egg-info/dependency_links.txt pyeapi.egg-info/requires.txt pyeapi.egg-info/top_level.txt pyeapi/api/__init__.py pyeapi/api/abstract.py pyeapi/api/acl.py pyeapi/api/bgp.py pyeapi/api/interfaces.py pyeapi/api/ipinterfaces.py pyeapi/api/mlag.py pyeapi/api/ntp.py pyeapi/api/ospf.py pyeapi/api/routemaps.py pyeapi/api/staticroute.py pyeapi/api/stp.py pyeapi/api/switchports.py pyeapi/api/system.py pyeapi/api/users.py pyeapi/api/varp.py pyeapi/api/vlans.py pyeapi/api/vrfs.py pyeapi/api/vrrp.py test/fixtures/dut.conf test/fixtures/eapi.conf test/fixtures/eapi.conf.yaml test/fixtures/empty.conf test/fixtures/env_path.conf test/fixtures/ipinterfaces.json test/fixtures/nohost.conf test/fixtures/running_config.bgp test/fixtures/running_config.ospf test/fixtures/running_config.portchannel test/fixtures/running_config.routemaps test/fixtures/running_config.text test/fixtures/running_config.varp test/fixtures/running_config.varp_null test/fixtures/running_config.vrf test/fixtures/running_config.vrrp test/fixtures/running_config.vxlan test/fixtures/show_interfaces.json test/fixtures/show_interfaces.text test/fixtures/show_portchannel.json test/fixtures/show_version.json test/fixtures/show_version.text test/fixtures/vxlan.json test/lib/systestlib.py test/lib/testlib.py test/system/test_api_acl.py test/system/test_api_interfaces.py test/system/test_api_ipinterfaces.py test/system/test_api_mlag.py test/system/test_api_ntp.py test/system/test_api_ospf.py test/system/test_api_routemaps.py test/system/test_api_staticroute.py test/system/test_api_stp.py test/system/test_api_switchports.py test/system/test_api_system.py test/system/test_api_users.py test/system/test_api_varp.py test/system/test_api_vlans.py test/system/test_api_vrfs.py test/system/test_api_vrrp.py test/system/test_client.py test/unit/test_api_acl.py test/unit/test_api_bgp.py test/unit/test_api_interfaces.py test/unit/test_api_ipinterfaces.py test/unit/test_api_mlag.py test/unit/test_api_ntp.py test/unit/test_api_ospf.py test/unit/test_api_routemaps.py test/unit/test_api_staticroute.py test/unit/test_api_stp.py test/unit/test_api_switchports.py test/unit/test_api_system.py test/unit/test_api_users.py test/unit/test_api_varp.py test/unit/test_api_vlans.py test/unit/test_api_vrfs.py test/unit/test_api_vrrp.py test/unit/test_client.py test/unit/test_eapilib.py test/unit/test_utils.pypyeapi-1.0.2/pyeapi.egg-info/dependency_links.txt0000644000076500000240000000000114447406213023173 0ustar dlyssenkostaff00000000000000 pyeapi-1.0.2/pyeapi.egg-info/requires.txt0000644000076500000240000000011014447406213021515 0ustar dlyssenkostaff00000000000000netaddr [dev] check-manifest pep8 pyflakes twine [test] coverage mock pyeapi-1.0.2/pyeapi.egg-info/top_level.txt0000644000076500000240000000000714447406213021654 0ustar dlyssenkostaff00000000000000pyeapi pyeapi-1.0.2/requirements.txt0000644000076500000240000000001014447405743017426 0ustar dlyssenkostaff00000000000000netaddr pyeapi-1.0.2/setup.cfg0000644000076500000240000000004614447406213015765 0ustar dlyssenkostaff00000000000000[egg_info] tag_build = tag_date = 0 pyeapi-1.0.2/setup.py0000644000076500000240000000541114447405743015666 0ustar dlyssenkostaff00000000000000from setuptools import setup, find_packages from codecs import open from os import path, environ import sys here = path.abspath(path.dirname(__file__)) # Get the long description from the relevant file with open(path.join(here, 'docs/description.rst'), encoding='utf-8') as f: long_description = f.read() with open(path.join(here, 'VERSION'), mode='r', encoding='utf-8') as version_file: version = version_file.read().strip() setup( name='pyeapi', # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html version=version, description='Python Client for eAPI', long_description=long_description, # The project's main homepage. url='https://github.com/arista-eosplus/pyeapi', # Author details author='Arista EOS+ CS', author_email='eosplus-dev@arista.com', # Choose your license license='BSD-3', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # How mature is this project? Common values are # 3 - Alpha # 4 - Beta # 5 - Production/Stable 'Development Status :: 4 - Beta', 'Intended Audience :: System Administrators', 'Topic :: System :: Networking', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3 :: Only' ], keywords='networking arista eos eapi', # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). packages=find_packages(exclude=['contrib', 'docs', 'tests*']), # List run-time dependencies here. These will be installed by pip when your # project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html install_requires=['netaddr'], # List additional dependencies here (e.g. development dependencies). # You can install these using the following syntax, for example: # $ pip install -e .[dev,test] extras_require={ 'dev': ['check-manifest', 'pep8', 'pyflakes', 'twine'], 'test': ['coverage', 'mock'], }, ) def install(): if "install" in sys.argv: return True else: return False # Use the following to dynamically build pyeapi module documentation if install() and environ.get('READTHEDOCS'): print('This method is only called by READTHEDOCS.') from subprocess import Popen proc = Popen(['make', 'modules'], cwd='docs/') (_, err) = proc.communicate() return_code = proc.wait() if return_code or err: raise ('Failed to make modules.(%s:%s)' % (return_code, err)) pyeapi-1.0.2/test/0000755000076500000240000000000014447406213015123 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/test/fixtures/0000755000076500000240000000000014447406213016774 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/test/fixtures/dut.conf0000644000076500000240000000013114447405743020441 0ustar dlyssenkostaff00000000000000[connection:veos01] host: 192.168.1.16 username: eapi password: password transport: http pyeapi-1.0.2/test/fixtures/eapi.conf0000644000076500000240000000035114447405743020567 0ustar dlyssenkostaff00000000000000[connection:test1] host: 192.168.1.16 username: eapi password: password enablepwd: enablepwd tags: tag1, tag2 [connection:test2] username: eapi password: password tags: tag1 [connection:localhost] username: eapi password: password pyeapi-1.0.2/test/fixtures/eapi.conf.yaml0000644000076500000240000000012214447405743021524 0ustar dlyssenkostaff00000000000000--- :username: admin :password: admin :use_ssl: true :port: 199 :hostname: bogus pyeapi-1.0.2/test/fixtures/empty.conf0000644000076500000240000000000014447405743020776 0ustar dlyssenkostaff00000000000000pyeapi-1.0.2/test/fixtures/env_path.conf0000644000076500000240000000013314447405743021453 0ustar dlyssenkostaff00000000000000[connection:env_path] host: 172.16.131.40 username: admin password: admin transport: https pyeapi-1.0.2/test/fixtures/ipinterfaces.json0000644000076500000240000000465314447405743022362 0ustar dlyssenkostaff00000000000000[ { "interfaces": { "Loopback0": { "interfaceAddress": { "secondaryIpsOrderedList": [], "broadcastAddress": "255.255.255.255", "secondaryIps": {}, "primaryIp": { "maskLen": 32, "address": "1.1.1.1" }, "virtualIp": { "maskLen": 0, "address": "0.0.0.0" } }, "name": "Loopback0", "urpf": "disable", "interfaceStatus": "connected", "enabled": true, "mtu": 65535, "vrf": "default", "localProxyArp": false, "proxyArp": false, "lineProtocolStatus": "up", "description": "managed by PE" }, "Management1": { "interfaceAddress": { "secondaryIpsOrderedList": [], "broadcastAddress": "255.255.255.255", "secondaryIps": {}, "primaryIp": { "maskLen": 24, "address": "192.168.1.16" }, "virtualIp": { "maskLen": 0, "address": "0.0.0.0" } }, "name": "Management1", "urpf": "disable", "interfaceStatus": "connected", "enabled": true, "mtu": 1500, "vrf": "default", "localProxyArp": false, "proxyArp": false, "lineProtocolStatus": "up", "description": "" }, "Vlan4094": { "interfaceAddress": { "secondaryIpsOrderedList": [], "broadcastAddress": "255.255.255.255", "secondaryIps": {}, "primaryIp": { "maskLen": 24, "address": "172.16.10.1" }, "virtualIp": { "maskLen": 0, "address": "0.0.0.0" } }, "name": "Vlan4094", "urpf": "disable", "interfaceStatus": "notconnect", "enabled": true, "mtu": 1478, "vrf": "default", "localProxyArp": false, "proxyArp": false, "lineProtocolStatus": "lowerLayerDown", "description": "autogenerated by eos_config::mlag" } } } ] pyeapi-1.0.2/test/fixtures/nohost.conf0000644000076500000240000000002314447405743021157 0ustar dlyssenkostaff00000000000000[connection:test] pyeapi-1.0.2/test/fixtures/running_config.bgp0000644000076500000240000000073414447405743022506 0ustar dlyssenkostaff00000000000000! ip routing ! router bgp 65000 router-id 1.1.1.1 neighbor test peer-group neighbor test remote-as 65001 neighbor test maximum-routes 12000 neighbor test1 peer-group neighbor test1 route-map RM-IN in neighbor test1 route-map RM-OUT out neighbor test1 maximum-routes 12000 neighbor 172.16.10.1 remote-as 65000 neighbor 172.16.10.1 maximum-routes 12000 neighbor 172.16.10.1 peer-group test network 172.16.10.0/24 network 172.17.0.0/16 ! ! pyeapi-1.0.2/test/fixtures/running_config.ospf0000644000076500000240000000157614447405743022712 0ustar dlyssenkostaff00000000000000! ip routing ! router ospf 65000 router-id 1.1.1.1 no bfd all-interfaces distance ospf intra-area 110 distance ospf external 110 distance ospf inter-area 110 redistribute bgp route-map RM-IN redistribute bgp route-map RM-OUT redistribute static area 0.0.0.0 default-cost 10 network 172.16.10.0/24 area 0.0.0.0 network 172.17.0.0/16 area 0.0.0.0 max-lsa 12000 75 ignore-time 5 ignore-count 5 reset-time 5 adjacency exchange-start threshold 20 log-adjacency-changes timers throttle spf 0 5000 5000 timers lsa arrival 1000 timers throttle lsa all 1000 5000 5000 no timers out-delay maximum-paths 128 no timers pacing flood no max-metric router-lsa point-to-point routes no graceful-restart ! router ospf 10 vrf test router-id 2.2.2.2 network 172.18.10.0/24 area 0.0.0.0 network 172.19.0.0/16 area 0.0.0.0 shutdown ! pyeapi-1.0.2/test/fixtures/running_config.portchannel0000644000076500000240000002137314447405743024255 0ustar dlyssenkostaff00000000000000! Command: show running-config all ! interface Port-Channel1 no description no shutdown default load-interval logging event link-status use-global switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no port-channel min-links no port-channel lacp fallback port-channel lacp fallback timeout 90 no l2 mtu no mlag no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group no switchport tool dot1q remove outer ! interface Ethernet5 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed no l2 mtu default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status channel-group 1 mode on lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group no switchport tool dot1q remove outer ! interface Ethernet6 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed no l2 mtu default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status channel-group 1 mode on lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group no switchport tool dot1q remove outer ! end pyeapi-1.0.2/test/fixtures/running_config.routemaps0000644000076500000240000000163214447405743023753 0ustar dlyssenkostaff00000000000000route-map TEST permit 10 set tag 50 match interface Ethernet1 continue 100 ! route-map TEST permit 20 match as 2000 match source-protocol ospf match interface Ethernet2 continue 200 ! route-map TEST deny 30 match as 2000 match source-protocol ospf match interface Ethernet2 continue 200 ! route-map FOO deny 20 match as 2000 match source-protocol ospf match interface Ethernet2 continue 200 ! route-map FOOBAR permit 20 match as 2000 match source-protocol ospf match interface Ethernet2 continue 200 ! route-map FOOBAR permit 20 match as 2000 match source-protocol ospf match interface Ethernet2 continue 200 ! route-map FOO-BAR permit 10 match as 2000 match source-protocol ospf match interface Ethernet2 continue 200 ! route-map FOO-BAR deny 20 match as 2000 match source-protocol ospf match interface Ethernet2 continue 200 ! pyeapi-1.0.2/test/fixtures/running_config.text0000644000076500000240000015617514447405743022735 0ustar dlyssenkostaff00000000000000! Command: show running-config all ! device: veos01 (vEOS, EOS-4.13.7M) ! ! boot system flash:/vEOS.swi ! bfd interval 300 min_rx 300 multiplier 3 default ! prompt %H%R%v%P no terminal length no terminal width alias pa bash sudo puppet agent --confdir /persist/local/puppet alias pr bash sudo puppet resource --confdir /persist/local/puppet ! schedule tech-support interval 60 max-log-files 100 command show tech-support ! cvx shutdown port 9979 heartbeat-interval 20 heartbeat-timeout 60 no service vxlan service openstack shutdown name-resolution interval 21600 no service debug service debug interval 1 ! no dcbx application ! no ip dhcp relay information option no ip dhcp relay always-on no ip dhcp smart-relay global ! no ip dhcp snooping no ip dhcp snooping information option ! directflow shutdown ! default switch forwarding-mode ! vlan internal allocation policy ascending ! email no from-user no server no auth username no auth password no tls ! errdisable detect cause link-flap no errdisable recovery cause bpduguard no errdisable recovery cause link-flap no errdisable recovery cause no-internal-vlan no errdisable recovery cause portchannelguard no errdisable recovery cause portsec no errdisable recovery cause tapagg no errdisable recovery cause uplink-failure-detection no errdisable recovery cause xcvr-unsupported errdisable recovery interval 300 ! ip igmp snooping no ip igmp snooping report-flooding ip igmp snooping robustness-variable 2 default ip igmp snooping vlan 1 default ip igmp snooping vlan 1 querier no ip igmp snooping vlan 1 querier address no ip igmp snooping vlan 1 querier query-interval no ip igmp snooping vlan 1 querier max-response-time no ip igmp snooping vlan 1 querier last-member-query-interval no ip igmp snooping vlan 1 querier version no ip igmp snooping vlan 1 max-groups ip igmp snooping vlan 1 immediate-leave default ip igmp snooping vlan 10 default ip igmp snooping vlan 10 querier no ip igmp snooping vlan 10 querier address no ip igmp snooping vlan 10 querier query-interval no ip igmp snooping vlan 10 querier max-response-time no ip igmp snooping vlan 10 querier last-member-query-interval no ip igmp snooping vlan 10 querier version no ip igmp snooping vlan 10 max-groups ip igmp snooping vlan 10 immediate-leave default ip igmp snooping vlan 100 default ip igmp snooping vlan 100 querier no ip igmp snooping vlan 100 querier address no ip igmp snooping vlan 100 querier query-interval no ip igmp snooping vlan 100 querier max-response-time no ip igmp snooping vlan 100 querier last-member-query-interval no ip igmp snooping vlan 100 querier version no ip igmp snooping vlan 100 max-groups ip igmp snooping vlan 100 immediate-leave default ip igmp snooping vlan 300 default ip igmp snooping vlan 300 querier no ip igmp snooping vlan 300 querier address no ip igmp snooping vlan 300 querier query-interval no ip igmp snooping vlan 300 querier max-response-time no ip igmp snooping vlan 300 querier last-member-query-interval no ip igmp snooping vlan 300 querier version no ip igmp snooping vlan 300 max-groups ip igmp snooping vlan 300 immediate-leave no ip igmp snooping querier no ip igmp snooping querier address ip igmp snooping querier query-interval 125 ip igmp snooping querier max-response-time 10 ip igmp snooping querier last-member-query-interval 1 no ip igmp snooping querier version ! default logging event congestion-drops ! default load-interval default ! transceiver qsfp default-mode 4x10G ! no ip pim rp-candidate no ip pim register-source no ip pim ssm range ip pim sparse-mode sg-expiry-timer 210 ip pim spt-threshold 0 ip pim log-neighbor-changes no ip pim bsr-holdtime no ip pim bsr-candidate no ip pim bfd ! ip msdp timer 30 ! lacp system-priority 32768 ! no queue-monitor length ! queue-monitor length global-buffer thresholds 512 256 no queue-monitor length mirror ! queue-monitor length global-buffer ! errdisable flap-setting cause link-flap max-flaps 5 time 10 ! lldp timer 30 lldp holdtime 120 lldp reinit 2 lldp tlv-select link-aggregation lldp tlv-select management-address lldp tlv-select max-frame-size lldp tlv-select port-description lldp tlv-select port-vlan lldp tlv-select system-capabilities lldp tlv-select system-description lldp tlv-select system-name lldp run no lldp management-address ! logging on logging buffered 32 debugging logging trap informational logging console errors no logging synchronous logging format timestamp traditional no logging format hostname fqdn logging facility local4 no logging source-interface ! logging level AAA debugging logging level ACCOUNTING debugging logging level ACL debugging logging level AGENT debugging logging level BFD debugging logging level BGP debugging logging level BRIDGING debugging logging level CAPI debugging logging level CLEAR debugging logging level DOT1X debugging logging level ENVMON debugging logging level ETH debugging logging level EVENTMON debugging logging level EXTENSION debugging logging level FHRP debugging logging level FLOW debugging logging level FRU debugging logging level FWK debugging logging level GMP debugging logging level HARDWARE debugging logging level IGMPSNOOPING debugging logging level INTF debugging logging level IP6ROUTING debugging logging level LACP debugging logging level LAG debugging logging level LAUNCHER debugging logging level LINEPROTO debugging logging level LLDP debugging logging level LOGMGR debugging logging level MAPREDUCEMONITOR debugging logging level MDIO debugging logging level MIRRORING debugging logging level MLAG debugging logging level MROUTE debugging logging level MRP debugging logging level MSDP debugging logging level MSRP debugging logging level MVRP debugging logging level NTP debugging logging level OPENFLOW debugging logging level OSPF debugging logging level OSPF3 debugging logging level PORTSECURITY debugging logging level PTP debugging logging level PWRMGMT debugging logging level QOS debugging logging level QUEUEMONITOR debugging logging level REACHABILITYMONITOR debugging logging level REDUNDANCY debugging logging level ROUTING debugging logging level SECURITY debugging logging level SPANTREE debugging logging level STAGEMGR debugging logging level SYS debugging logging level SYSDB debugging logging level TAPAGG debugging logging level TCP debugging logging level TRANSCEIVER debugging logging level VM debugging logging level VMTRACERSESS debugging logging level VMWAREVI debugging logging level VMWAREVS debugging logging level VRF debugging logging level VRRP debugging logging level VXLAN debugging logging level XMPP debugging logging level ZTP debugging ! logging event link-status global ! default mrp leave-timer default mrp leave-all-timer ! no msrp streams load-file ! hostname veos01 no ip domain lookup source-interface ip name-server vrf default 192.168.1.32 ip domain-name stormcontrol.net ip host puppet 192.168.1.130 ! no ntp trusted-key no ntp authenticate ntp source Loopback1 ntp server 1.2.3.4 prefer ntp server 10.20.30.40 ntp server 11.22.33.44 ntp server 123.33.22.11 prefer ntp server 123.44.55.66 ntp server 192.168.1.32 iburst no ntp serve all ! power poll-interval 5 ! no radius-server key radius-server timeout 5 radius-server retransmit 3 no radius-server deadtime no radius-server attribute 32 include-in-access-req format ! sflow sample 1048576 sflow polling-interval 2 no sflow source no sflow source-interface sflow sample output interface no sflow run ! no snmp-server engineID local no snmp-server chassis-id no snmp-server contact no snmp-server location no snmp-server source-interface default snmp-server enable traps default snmp-server enable traps bgp default snmp-server enable traps bgp arista-backward-transition default snmp-server enable traps bgp arista-established default snmp-server enable traps bgp backward-transition default snmp-server enable traps bgp established default snmp-server enable traps entity default snmp-server enable traps entity arista-ent-sensor-alarm default snmp-server enable traps entity ent-config-change default snmp-server enable traps entity ent-state-oper-disabled default snmp-server enable traps entity ent-state-oper-enabled default snmp-server enable traps lldp default snmp-server enable traps lldp rem-tables-change default snmp-server enable traps msdp default snmp-server enable traps msdp backward-transition default snmp-server enable traps msdp established default snmp-server enable traps ospf default snmp-server enable traps ospf if-auth-failure default snmp-server enable traps ospf if-config-error default snmp-server enable traps ospf if-state-change default snmp-server enable traps ospf nbr-state-change default snmp-server enable traps pim default snmp-server enable traps pim neighbor-loss default snmp-server enable traps snmp default snmp-server enable traps snmp authentication default snmp-server enable traps snmp link-down default snmp-server enable traps snmp link-up default snmp-server enable traps snmpConfigManEvent default snmp-server enable traps snmpConfigManEvent arista-config-man-event default snmp-server enable traps switchover default snmp-server enable traps switchover arista-redundancy-switch-over-notif default snmp-server enable traps test default snmp-server enable traps test arista-test-notification default snmp-server enable traps vrrp default snmp-server enable traps vrrp trap-new-master snmp-server vrf default ! spanning-tree mode mstp spanning-tree hello-time 2000 spanning-tree max-age 20 spanning-tree forward-time 15 spanning-tree transmit hold-count 6 spanning-tree max-hops 20 no spanning-tree portfast bpduguard default no spanning-tree portfast bpdufilter default spanning-tree transmit active no spanning-tree guard loop default no spanning-tree portchannel guard misconfig spanning-tree bpduguard rate-limit default logging event spanning-tree global spanning-tree mst 0 priority 32768 ! spanning-tree mst configuration no name revision 0 ! no service sequence-numbers no service running-config timestamp ! no tacacs-server key tacacs-server timeout 5 ! aaa authentication login default local no aaa authentication login console aaa authentication enable default local no aaa authentication policy on-success log no aaa authentication policy on-failure log no aaa authorization console no aaa authorization exec default no aaa authorization commands all default aaa authorization config-commands no aaa accounting exec console no aaa accounting commands all console no aaa accounting exec default no aaa accounting system default no aaa accounting dot1x default no aaa accounting commands all default ! no enable password no aaa root aaa authentication policy local allow-nopassword-remote-login no aaa authorization policy local default-role ! username admin privilege 1 role network-admin nopassword username eapi privilege 1 secret 5 $1$eCurfHLe$JCbuUNM7Xwy6i6/zknYha. username test privilege 10 nopassword username test1 privilege 1 secret 5 $1$o/po05ru$92uegC/GGu3i4MS7MH9AE0 username test2 privilege 9 role ops secret 5 $1$Kraw0Knu$dfIURYuRCxzDDcyyKnAD1/ username test3 privilege 9 role ops nopassword ! role network-admin 10 permit command .* ! role network-operator 10 deny mode exec command configure|bash|python-shell|\| 20 permit mode exec command .* ! tap aggregation no mode ! environment overheat action shutdown environment insufficient-fans action shutdown environment fan-speed auto ! clock timezone US/Eastern ! no virtual-cable ! vlan 1 name default state active no private-vlan ! vlan 10 name VLAN0010 state active no private-vlan trunk group tg1 ! vlan 100 name mytest state active no private-vlan ! vlan 200-202,204 name grouping state active no private-vlan ! vlan 300 name VLAN0300 state active no private-vlan ! interface Port-Channel10 no description no shutdown default load-interval logging event link-status use-global switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no port-channel min-links no port-channel lacp fallback port-channel lacp fallback timeout 90 no l2 mtu mlag 10 no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group no switchport tool dot1q remove outer ! interface Ethernet1 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning switchport trunk group foo switchport trunk group bar no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no channel-group lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no storm-control broadcast no storm-control multicast no storm-control all no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group ! interface Ethernet2 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no channel-group lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no storm-control broadcast no storm-control multicast no storm-control all no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group ! interface Ethernet3 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no channel-group lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no storm-control broadcast no storm-control multicast no storm-control all no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group ! interface Ethernet4 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no channel-group lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no storm-control broadcast no storm-control multicast no storm-control all no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group ! interface Ethernet5 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no channel-group lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no storm-control broadcast no storm-control multicast no storm-control all no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group ! interface Ethernet6 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no channel-group lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no storm-control broadcast no storm-control multicast no storm-control all no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group ! interface Ethernet7 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no channel-group lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no storm-control broadcast no storm-control multicast no storm-control all no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group ! interface Ethernet48.2044 description some port description no shutdown default load-interval logging event link-status use-global encapsulation dot1q vlan 2044 snmp trap link-change vrf DAT no ip proxy-arp no ip local-proxy-arp no arp gratuitous accept ip address 100.76.1.41/31 no ip verify unicast no ip directed-broadcast ip attached-routes default arp aging timeout default ipv6 nd cache expire no bfd echo no bfd authentication mode default ip dhcp relay all-subnets no ip helper-address no ipv6 dhcp relay destination no ipv6 dhcp relay add vendor-option ccap-core no ipv6 dhcp relay install routes ip dhcp relay information option circuit-id Ethernet48.2044 no dhcp server ipv4 no dhcp server ipv6 no ip attached-host route export no ipv6 attached-host route export no ip igmp ip igmp version 3 ip igmp last-member-query-count 2 ip igmp last-member-query-interval 10 igmp query-max-response-time 100 ip igmp query-interval 125 ip igmp startup-query-count 2 ip igmp startup-query-interval 310 ip igmp router-alert optional connected no ip igmp host-proxy no ipv6 enable default ipv6 nd dad no ipv6 address no ipv6 nd ra rx accept default-route ipv6 attached-routes no ipv6 verify unicast no ipv6 nd ra disabled ipv6 nd ra interval msec 200000 ipv6 nd ra lifetime 1800 no ipv6 nd ra mtu suppress no ipv6 nd managed-config-flag no ipv6 nd other-config-flag ipv6 nd reachable-time 0 ipv6 nd router-preference medium ipv6 nd ra dns-servers lifetime 300 ipv6 nd ra dns-suffixes lifetime 300 ipv6 nd ra hop-limit 64 no tcp mss ceiling no multicast ipv4 source route export no multicast ipv6 source route export no multicast ipv4 static no multicast ipv6 static mfib ipv4 fastdrop no mld no mld static-group access-list mld query-interval 125 mld query-response-interval 10 no mld startup-query-interval mld startup-query-count 2 mld robustness 2 mld last-listener-query-interval 1 mld last-listener-query-count 2 mpls ip default ntp serve no pim ipv4 sparse-mode no pim ipv4 bidirectional no pim ipv4 border-router pim ipv4 hello interval 30 pim ipv4 hello count 3.5 pim ipv4 dr-priority 1 pim ipv4 join-prune interval 60 pim ipv4 join-prune count 3.5 no pim ipv4 neighbor filter default pim ipv4 bfd no pim ipv4 join-prune transport sctp no pim ipv4 local-interface no pim ipv4 non-dr install-oifs no pim ipv6 sparse-mode no pim ipv6 border-router pim ipv6 hello interval 30 pim ipv6 hello count 3.5 pim ipv6 dr-priority 1 pim ipv6 join-prune interval 60 pim ipv6 join-prune count 3.5 no pim ipv6 neighbor filter default pim ipv6 bfd no pim bsr ipv4 border no pim bsr ipv6 border no rip v2 multicast disable no node-segment ipv4 index no node-segment ipv6 index ! interface Loopback0 no description no shutdown default load-interval mtu 1500 no switchport logging event link-status use-global snmp trap link-status no ipv6 enable no ipv6 address no ipv6 verify unicast no ipv6 nd ra suppress ipv6 nd ra interval 200 ipv6 nd ra lifetime 1800 no ipv6 nd ra mtu suppress no ipv6 nd managed-config-flag no ipv6 nd other-config-flag ipv6 nd reachable-time 0 ipv6 nd router-preference medium ipv6 nd ra dns-servers lifetime 300 ipv6 nd ra dns-suffixes lifetime 300 ipv6 nd ra hop-limit 64 no ip proxy-arp no ip local-proxy-arp ip address 1.1.1.1/32 no ip verify unicast default arp timeout 14400 default ipv6 nd cache expire 14400 bfd interval 300 min_rx 300 multiplier 3 default ntp serve ! interface Loopback2 description test fixture with secondary ip ip address 2.2.2.2/32 ip address 3.255.255.1/24 secondary ip address 4.255.255.1/24 secondary ! interface Ethernet8 ! the interface config is added separately covering test for issue #213 ! it might be inconsistend with the rest of config, though all unit tests pass profile VLAN20 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no l2-protocol forwarding profile no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed no l2 mtu no l2 mru default logging event congestion-drops default unidirectional no traffic-loopback no mac-address default error-correction encoding no error-correction reed-solomon bypass phy media 25gbase-cr negotiation standard consortium ieee switchport trunk native vlan 1 no switchport phone vlan no switchport phone trunk no switchport vlan translation in required no switchport dot1q vlan tag switchport trunk allowed vlan 1-4094 switchport mode access switchport dot1q ethertype 0x8100 switchport mac address learning default bridge mac-address-table aging timeout no switchport vlan forwarding accept all switchport no encapsulation dot1q vlan default switchport source-interface tx no switchport trunk private-vlan secondary no switchport pvlan mapping no l2-protocol encapsulation dot1q vlan 0 no flow tracker hardware no flow tracker sampled no flow-spec ipv4 ipv6 snmp trap link-change no address locking ipv4 no ip proxy-arp no ip local-proxy-arp no arp gratuitous accept no ip address no ip verify unicast no ip directed-broadcast ip attached-routes default arp aging timeout default arp cache dynamic capacity default ipv6 nd cache expire default ipv6 nd cache dynamic capacity bfd interval 300 min_rx 300 multiplier 3 no bfd echo no bfd authentication mode default ip dhcp relay all-subnets default ipv6 dhcp relay all-subnets no ip helper-address no ipv6 dhcp relay destination no ipv6 dhcp relay add vendor-option ccap-core no ipv6 dhcp relay install routes ip dhcp relay information option circuit-id Ethernet1 no dhcp relay ipv4 disabled no dhcp relay ipv6 disabled no dhcp server ipv4 no dhcp server ipv6 no ip igmp ip igmp version 3 ip igmp last-member-query-count 2 ip igmp last-member-query-interval 10 igmp query-max-response-time 100 ip igmp query-interval 125 ip igmp startup-query-count 2 ip igmp startup-query-interval 310 ip igmp router-alert optional connected no ip igmp host-proxy no ipv6 enable default ipv6 nd dad no ipv6 address no ipv6 nd ra rx accept default-route ipv6 attached-routes no ipv6 verify unicast no ipv6 nd ra disabled ipv6 nd ra interval msec 200000 ipv6 nd ra lifetime 1800 no ipv6 nd ra mtu suppress no ipv6 nd ra ns-interval unspecified no ipv6 nd managed-config-flag no ipv6 nd other-config-flag ipv6 nd reachable-time 0 ipv6 nd router-preference medium ipv6 nd ra dns-servers lifetime 300 ipv6 nd ra dns-suffixes lifetime 300 ipv6 nd ra hop-limit 64 no tcp mss ceiling no lacp port-id no channel-group lacp timer normal lacp timer multiplier 3 lacp port-priority 32768 default lacp rate-limit no forwarding port-channel transmit disabled lldp transmit lldp receive no lldp tlv transmit power-via-mdi fallback allocated-power no lldp tlv transmit ztp vlan 2 no lldp med network-policy no mdns ipv4 no multicast ipv4 static no multicast ipv6 static mfib ipv4 fastdrop no mld no mld static-group access-list mld query-interval 125 mld query-response-interval 10 no mld startup-query-interval mld startup-query-count 2 mld robustness 2 mld last-listener-query-interval 1 mld last-listener-query-count 2 mpls ip default mrp leave-timer no msrp no mvrp default ntp serve no phy transmitter clock shift no phy link detection aggressive no phy link transmitter recovery no phy link training no phy link stabilization remote-fault disabled no phy media base-t negotiation port type no phy link serdes mapping direct no phy media base-t polarity pair a no phy media base-t polarity pair b no phy media base-t polarity pair c no phy media base-t polarity pair d no pim ipv4 sparse-mode no pim ipv4 bidirectional no pim ipv4 border-router pim ipv4 hello interval 30 pim ipv4 hello count 3.5 pim ipv4 dr-priority 1 pim ipv4 join-prune interval 60 pim ipv4 join-prune count 3.5 no pim ipv4 neighbor filter default pim ipv4 bfd no pim ipv4 join-prune transport sctp no pim ipv4 local-interface no pim ipv4 non-dr install-oifs no pim ipv6 sparse-mode no pim ipv6 border-router pim ipv6 hello interval 30 pim ipv6 hello count 3.5 pim ipv6 dr-priority 1 pim ipv6 join-prune interval 60 pim ipv6 join-prune count 3.5 no pim ipv6 neighbor filter default pim ipv6 bfd no pim bsr ipv4 border no pim bsr ipv6 border no poe priority no poe pairset default poe reboot action default poe link down action default poe shutdown action poe no poe limit poe negotiation lldp no poe legacy detect switchport port-security mac-address maximum 1 no switchport port-security default qos trust qos cos 0 qos dscp 0 no shape rate no priority-flow-control priority-flow-control pause watchdog no priority-flow-control priority 0 no priority-flow-control priority 1 no priority-flow-control priority 2 no priority-flow-control priority 3 no priority-flow-control priority 4 no priority-flow-control priority 5 no priority-flow-control priority 6 no priority-flow-control priority 7 no priority-flow-control pause watchdog port action no priority-flow-control pause watchdog port timer ! tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed no random-detect ecn minimum-threshold no random-detect drop no random-detect non-ect no random-detect ecn count ! tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed no random-detect ecn minimum-threshold no random-detect drop no random-detect non-ect no random-detect ecn count ! tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed no random-detect ecn minimum-threshold no random-detect drop no random-detect non-ect no random-detect ecn count ! tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed no random-detect ecn minimum-threshold no random-detect drop no random-detect non-ect no random-detect ecn count ! tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed no random-detect ecn minimum-threshold no random-detect drop no random-detect non-ect no random-detect ecn count ! tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed no random-detect ecn minimum-threshold no random-detect drop no random-detect non-ect no random-detect ecn count ! tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed no random-detect ecn minimum-threshold no random-detect drop no random-detect non-ect no random-detect ecn count ! tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed no random-detect ecn minimum-threshold no random-detect drop no random-detect non-ect no random-detect ecn count no rip v2 multicast disable no rip authentication algorithm md5 tx key-id default sflow enable default sflow egress enable no node-segment ipv4 index no node-segment ipv6 index no storm-control broadcast no storm-control multicast no storm-control unknown-unicast no storm-control all logging event storm-control discards use-global no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity no switchport tap encapsulation vxlan strip no switchport tap encapsulation gre strip no switchport tap encapsulation gre protocol strip no switchport tap mpls pop all no switchport tool mpls pop all no switchport tool encapsulation vn-tag strip no switchport tool encapsulation dot1br strip switchport tap allowed vlan 1-4095 switchport tool allowed vlan 1-4095 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tap default interface no switchport tool group no switchport tool dot1q remove outer ! interface Management1 no description no shutdown default load-interval mtu 1500 logging event link-status use-global no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed default logging event congestion-drops default unidirectional snmp trap link-status no ipv6 enable no ipv6 address no ipv6 verify unicast ipv6 nd ra suppress all ipv6 nd ra interval 200 ipv6 nd ra lifetime 1800 no ipv6 nd ra mtu suppress no ipv6 nd managed-config-flag no ipv6 nd other-config-flag ipv6 nd reachable-time 0 ipv6 nd router-preference medium ipv6 nd ra dns-servers lifetime 300 ipv6 nd ra dns-suffixes lifetime 300 ipv6 nd ra hop-limit 64 no ip proxy-arp no ip local-proxy-arp ip address 192.168.1.16/24 no ip verify unicast default arp timeout 14400 default ipv6 nd cache expire 14400 bfd interval 300 min_rx 300 multiplier 3 lldp transmit lldp receive default ntp serve ! interface Vlan10 no description no shutdown default load-interval mtu 1500 logging event link-status use-global autostate no private-vlan mapping snmp trap link-status no ipv6 enable no ipv6 address no ipv6 verify unicast no ipv6 nd ra suppress ipv6 nd ra interval 200 ipv6 nd ra lifetime 1800 no ipv6 nd ra mtu suppress no ipv6 nd managed-config-flag no ipv6 nd other-config-flag ipv6 nd reachable-time 0 ipv6 nd router-preference medium ipv6 nd ra dns-servers lifetime 300 ipv6 nd ra dns-suffixes lifetime 300 ipv6 nd ra hop-limit 64 no ip proxy-arp no ip local-proxy-arp ip address 6.7.5.6/24 no ip verify unicast default arp timeout 14400 default ipv6 nd cache expire 14400 bfd interval 300 min_rx 300 multiplier 3 default ip dhcp smart-relay no ip helper-address no ipv6 dhcp relay destination ip dhcp relay information option circuit-id Vlan10 ip igmp last-member-query-count 2 ip igmp last-member-query-interval 10 ip igmp query-max-response-time 100 ip igmp query-interval 125 ip igmp startup-query-count 2 ip igmp startup-query-interval 310 ip igmp host-proxy no ip igmp host-proxy report-interval no ip igmp host-proxy ip mfib fastdrop default ntp serve no ip pim sparse-mode no ip pim border-router ip pim query-interval 30 ip pim join-prune-interval 60 ip pim dr-priority 1 no ip pim neighbor-filter no ip pim bsr-border default ip pim bfd-instance ! ip access-list standard test no statistics per-entry fragment-rules 10 permit host 1.2.3.4 log 20 permit 1.2.3.4 255.255.0.0 log 30 deny any 40 permit 5.6.7.0/24 50 permit 16.0.0.0/8 60 permit any log ! ip access-list exttest no statistics per-entry fragment-rules 10 permit tcp host 1.1.1.1 eq telnet host 2.2.2.2 log 20 permit icmp 3.3.3.0/24 any ttl eq 2 tracked log 30 permit tcp host 7.7.7.7 eq https any eq https log 40 permit udp 4.4.0.0/16 any eq pkt-krb-ipsec 50 permit ip any host 1.1.1.2 60 deny ip any any log 70 permit tcp 8.8.8.0/24 neq irc host 3.3.3.3 lt ipp urg ttl eq 24 fragments tracked log ! mac address-table aging-time 300 ! monitor hadoop shutdown ! no ip fhrp accept-mode ! no ip virtual-router mac-address ip virtual-router mac-address advertisement-interval 30 ! no ipv6 hardware fib nexthop-index ! ip route 0.0.0.0/0 192.68.1.254 1 tag 0 ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 ! ip icmp redirect ip routing ! no ip multicast-routing no ip multicast-routing static no ip multicast multipath none ip mfib activity polling-interval 60 ip mfib max-fastdrops 1024 ip mfib cache-entries unresolved max 4000 ip mfib packet-buffers unresolved max 3 ! ip as-path regex-mode asn ! no ipv6 unicast-routing ! control-plane ip access-group default-control-plane-acl in ipv6 access-group default-control-plane-acl in ! mac address-table notification host-flap logging mac address-table notification host-flap detection window 15 ! mlag configuration no domain-id heartbeat-interval 2000 no local-interface no peer-address no peer-link reload-delay 300 no reload-delay non-mlag no reload-delay mode no shutdown ! no mpls routing ! no ip ftp client source-interface no ip http client source-interface no ip ssh client source-interface no ip tftp client source-interface ! no qos random-detect ecn global-buffer qos map cos 0 1 2 3 4 5 6 7 to traffic-class 8 qos map dscp 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 to traffic-class 9 qos map traffic-class 0 1 2 3 4 5 6 7 8 9 10 11 to cos 3 qos map traffic-class 0 1 2 3 4 5 6 7 8 9 10 11 to uc-tx-queue 4 qos map traffic-class 0 1 2 3 4 5 6 7 8 9 10 11 to mc-tx-queue 16 ! policy-map type control-plane copp-system-policy class copp-system-bpdu shape pps 6000 bandwidth pps 5000 class copp-system-arp shape pps 25000 bandwidth pps 1000 class copp-system-igmp shape pps 5000 bandwidth pps 4000 class copp-system-default no shape no bandwidth ! no ip radius source-interface ! monitor reachability shutdown probe receiver max-streams 50000 probe checkpoint-interval 60 destination port 49152 default ignore-checksum default preserve-streams ! no ip tacacs source-interface ! no vxlan vni notation dotted ! banner login this is the loging ban that would b emult EOF banner motd this text can be multine EOF ! system coredump compressed ! no dot1x system-auth-control ! management api http-commands vrf default default shutdown ! management api http-commands no protocol https protocol http default protocol https certificate default cors allowed-origin default protocol https cipher default protocol https key-exchange default protocol https mac no shutdown ! management cim-provider shutdown http 7778 https 7779 idle-timeout 90 default live-time default trace default ssl certificate default ssl key ! management console idle-timeout 0 ! management cvx shutdown no server host no source-interface no server port heartbeat-interval 20 heartbeat-timeout 60 no service debug service debug interval 1 ! management defaults secret hash md5 ! management security no entropy source hardware no password minimum length ! management ssh idle-timeout 0 authentication mode keyboard-interactive server-port 22 no fips restrictions no hostkey client strict-checking no shutdown login timeout 120 log-level info ! management telnet shutdown idle-timeout 0 ! management xmpp shutdown vrf default session privilege 1 ! ! end pyeapi-1.0.2/test/fixtures/running_config.varp0000644000076500000240000000063114447405743022702 0ustar dlyssenkostaff00000000000000ip virtual-router mac-address 00:11:22:33:44:55 interface Vlan4001 ip address 1.1.1.1/24 ip virtual-router address 1.1.1.2 ! interface Vlan4002 ip address 1.1.2.1/24 ip virtual-router address 1.1.2.2 ip virtual-router address 1.1.2.3 ip virtual-router address 1.1.2.4 ip virtual-router address 1.1.2.5 ip virtual-router address 1.1.2.6 ! interface Vlan4003 ip address 1.1.2.1/24 ! pyeapi-1.0.2/test/fixtures/running_config.varp_null0000644000076500000240000000077414447405743023744 0ustar dlyssenkostaff00000000000000interface Vlan4001 ip address 1.1.1.1/24 ip virtual-router address 1.1.1.2 ip virtual-router address 1.1.1.3 ip virtual-router address 1.1.1.4 ip virtual-router address 1.1.1.5 ip virtual-router address 1.1.1.6 ! interface Vlan4002 ip address 1.1.2.1/24 ip virtual-router address 1.1.2.2 ip virtual-router address 1.1.2.3 ip virtual-router address 1.1.2.4 ip virtual-router address 1.1.2.5 ip virtual-router address 1.1.2.6 ! interface Vlan4003 ip address 1.1.2.1/24 ! pyeapi-1.0.2/test/fixtures/running_config.vrf0000644000076500000240000001264614447405743022540 0ustar dlyssenkostaff00000000000000logging level VRF debugging ! no snmp-server vrf default source-interface snmp-server vrf default ! vrf definition blah rd 10:10 description blah desc ! vrf definition second no description ! vrf definition test rd 200:500 no description ! interface Ethernet1 no description no shutdown default load-interval mtu 1500 logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed no l2 mtu default logging event congestion-drops default unidirectional no traffic-loopback default error-correction encoding no error-correction reed-solomon bypass switchport dot1q ethertype 0x8100 no switchport no encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status vrf forwarding blah no ip proxy-arp no ip local-proxy-arp ip address 10.10.10.1/24 no ip verify unicast default arp timeout 14400 default ipv6 nd cache expire 14400 bfd interval 300 min_rx 300 multiplier 3 no bfd echo default ip dhcp smart-relay no ip helper-address no ipv6 dhcp relay destination ip dhcp relay information option circuit-id Ethernet1 no ip igmp ip igmp version 3 ip igmp last-member-query-count 2 ip igmp last-member-query-interval 10 ip igmp query-max-response-time 100 ip igmp query-interval 125 ip igmp startup-query-count 2 ip igmp startup-query-interval 310 ip igmp router-alert optional connected no ip igmp host-proxy no ipv6 enable no ipv6 address no ipv6 verify unicast no ipv6 nd ra suppress ipv6 nd ra interval msec 200000 ipv6 nd ra lifetime 1800 no ipv6 nd ra mtu suppress no ipv6 nd managed-config-flag no ipv6 nd other-config-flag ipv6 nd reachable-time 0 ipv6 nd router-preference medium ipv6 nd ra dns-servers lifetime 300 ipv6 nd ra dns-suffixes lifetime 300 ipv6 nd ra hop-limit 64 no channel-group lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no ip multicast static ip mfib fastdrop mpls ip no msrp no mvrp default ntp serve no ip pim sparse-mode no ip pim bidirectional no ip pim border-router ip pim query-interval 30 ip pim query-count 3.5 ip pim join-prune-interval 60 ip pim dr-priority 1 no ip pim neighbor-filter default ip pim bfd-instance no ip pim bsr-border default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed no ip rip v2-broadcast sflow enable no ip virtual address ! ip routing vrf blah no ip icmp source-interface vrf blah no ip routing vrf second no ip icmp source-interface vrf second no ip routing vrf test no ip icmp source-interface vrf test ! no ipv6 unicast-routing vrf blah no ipv6 unicast-routing vrf second ipv6 unicast-routing vrf test ! control-plane ip access-group default-control-plane-acl in ip access-group default-control-plane-acl vrf blah in ip access-group default-control-plane-acl vrf second in ip access-group default-control-plane-acl vrf test in ipv6 access-group default-control-plane-acl in ipv6 access-group default-control-plane-acl vrf blah in ipv6 access-group default-control-plane-acl vrf second in ipv6 access-group default-control-plane-acl vrf test in ! management api http-commands protocol https port 443 protocol http no protocol http localhost port 8080 no protocol unix-socket no protocol https certificate no protocol https ssl profile no cors allowed-origin protocol https cipher aes256-cbc aes128-cbc protocol https key-exchange rsa diffie-hellman-ephemeral-rsa protocol https mac hmac-sha1 qos dscp 0 no shutdown vrf default no shutdown ! management cvx shutdown no server host no source-interface heartbeat-interval 20 heartbeat-timeout 60 no ssl profile vrf default service debug no shutdown interval 1 ! management xmpp shutdown no connection unencrypted permit vrf default session privilege 1 ! pyeapi-1.0.2/test/fixtures/running_config.vrrp0000644000076500000240000003203714447405743022730 0ustar dlyssenkostaff00000000000000! logging level VRRP debugging ! default snmp-server enable traps vrrp default snmp-server enable traps vrrp trap-new-master ! interface Port-Channel1 no description no shutdown default load-interval logging event link-status use-global switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no port-channel min-links no port-channel lacp fallback port-channel lacp fallback timeout 90 no l2 mtu no mlag no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group no switchport tool dot1q remove outer ! interface Port-Channel10 no description no shutdown default load-interval logging event link-status use-global switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no ip proxy-arp no ip local-proxy-arp ip address 10.10.5.1/24 no ip verify unicast no port-channel min-links no port-channel lacp fallback port-channel lacp fallback timeout 90 no l2 mtu no mlag no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group no switchport tool dot1q remove outer vrrp 10 priority 150 vrrp 10 timers advertise 1 vrrp 10 mac-address advertisement-interval 30 vrrp 10 preempt vrrp 10 preempt delay minimum 0 vrrp 10 preempt delay reload 0 vrrp 10 delay reload 0 no vrrp 10 authentication vrrp 10 ip 10.10.5.10 vrrp 10 ip 10.10.5.20 secondary vrrp 10 ipv6 :: vrrp 10 description vrrp 10 on Port-Channel10 no vrrp 10 shutdown no vrrp 10 bfd ip no vrrp 10 bfd ipv6 vrrp 10 ip version 2 ! interface Ethernet1 no description no shutdown default load-interval logging event link-status use-global no dcbx mode no mac-address no link-debounce no flowcontrol send no flowcontrol receive no mac timestamp no speed no l2 mtu default logging event congestion-drops default unidirectional switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no ip proxy-arp no ip local-proxy-arp ip address 10.10.6.1/24 no ip verify unicast no channel-group lacp rate normal lacp port-priority 32768 lldp transmit lldp receive no msrp no mvrp no switchport port-security switchport port-security maximum 1 default qos trust qos cos 5 qos dscp 2 no shape rate mc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! mc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 0 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 1 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 2 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 3 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 4 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 5 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 6 priority strict no bandwidth percent no shape rate no bandwidth guaranteed ! uc-tx-queue 7 priority strict no bandwidth percent no shape rate no bandwidth guaranteed sflow enable no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group no switchport tool dot1q remove outer vrrp 10 priority 175 vrrp 10 timers advertise 1 vrrp 10 mac-address advertisement-interval 30 vrrp 10 preempt vrrp 10 preempt delay minimum 0 vrrp 10 preempt delay reload 0 vrrp 10 delay reload 0 no vrrp 10 authentication vrrp 10 ip 10.10.6.10 vrrp 10 ipv6 :: vrrp 10 description vrrp 10 on Ethernet1 no vrrp 10 shutdown no vrrp 10 bfd ip no vrrp 10 bfd ipv6 vrrp 10 ip version 2 ! interface Vlan50 no description no shutdown default load-interval mtu 1500 logging event link-status use-global autostate no private-vlan mapping snmp trap link-status no ip proxy-arp no ip local-proxy-arp ip address 10.10.4.1/24 no ip verify unicast default arp timeout 14400 default ipv6 nd cache expire 14400 bfd interval 300 min_rx 300 multiplier 3 no bfd echo default ip dhcp smart-relay no ip helper-address no ipv6 dhcp relay destination ip dhcp relay information option circuit-id Vlan50 no ip igmp ip igmp version 3 ip igmp last-member-query-count 2 ip igmp last-member-query-interval 10 ip igmp query-max-response-time 100 ip igmp query-interval 125 ip igmp startup-query-count 2 ip igmp startup-query-interval 310 ip igmp router-alert optional connected ip igmp host-proxy no ip igmp host-proxy report-interval ip igmp host-proxy version 3 no ip igmp host-proxy no ipv6 enable no ipv6 address no ipv6 verify unicast no ipv6 nd ra suppress ipv6 nd ra interval msec 200000 ipv6 nd ra lifetime 1800 no ipv6 nd ra mtu suppress no ipv6 nd managed-config-flag no ipv6 nd other-config-flag ipv6 nd reachable-time 0 ipv6 nd router-preference medium ipv6 nd ra dns-servers lifetime 300 ipv6 nd ra dns-suffixes lifetime 300 ipv6 nd ra hop-limit 64 ip mfib fastdrop default ntp serve no ip pim sparse-mode no ip pim border-router ip pim query-interval 30 ip pim join-prune-interval 60 ip pim dr-priority 1 no ip pim neighbor-filter default ip pim bfd-instance no ip pim bsr-border no ip virtual address vrrp 10 priority 200 vrrp 10 timers advertise 3 vrrp 10 mac-address advertisement-interval 30 vrrp 10 preempt vrrp 10 preempt delay minimum 0 vrrp 10 preempt delay reload 0 vrrp 10 delay reload 0 no vrrp 10 authentication vrrp 10 ip 10.10.4.10 vrrp 10 ip 10.10.4.21 secondary vrrp 10 ip 10.10.4.22 secondary vrrp 10 ip 10.10.4.23 secondary vrrp 10 ip 10.10.4.24 secondary vrrp 10 ipv6 :: no vrrp 10 description no vrrp 10 shutdown vrrp 10 track Ethernet1 decrement 10 vrrp 10 track Ethernet1 shutdown vrrp 10 track Ethernet2 decrement 50 vrrp 10 track Ethernet2 shutdown vrrp 10 track Ethernet11 decrement 75 vrrp 10 track Ethernet11 shutdown no vrrp 10 bfd ip no vrrp 10 bfd ipv6 vrrp 10 ip version 2 vrrp 20 priority 100 vrrp 20 timers advertise 5 vrrp 20 mac-address advertisement-interval 30 no vrrp 20 preempt vrrp 20 preempt delay minimum 0 vrrp 20 preempt delay reload 0 vrrp 20 delay reload 0 vrrp 20 authentication text 12345 vrrp 20 ip 10.10.4.20 vrrp 20 ipv6 :: no vrrp 20 description vrrp 20 shutdown vrrp 20 track Ethernet1 shutdown vrrp 20 track Ethernet2 decrement 1 vrrp 20 track Ethernet2 shutdown no vrrp 20 bfd ip no vrrp 20 bfd ipv6 vrrp 20 ip version 2 vrrp 30 priority 50 vrrp 30 timers advertise 1 vrrp 30 mac-address advertisement-interval 30 vrrp 30 preempt vrrp 30 preempt delay minimum 0 vrrp 30 preempt delay reload 0 vrrp 30 delay reload 0 vrrp 30 authentication ietf-md5 key-string 7 bu1yTgzm0RDgraNS0MNkaA== vrrp 30 ip 10.10.4.30 vrrp 30 ipv6 :: no vrrp 30 description no vrrp 30 shutdown vrrp 30 bfd ip 10.10.4.33 no vrrp 30 bfd ipv6 vrrp 30 ip version 2 ! ! pyeapi-1.0.2/test/fixtures/running_config.vxlan0000644000076500000240000000301014447405743023054 0ustar dlyssenkostaff00000000000000interface Vxlan1 no description no shutdown default load-interval logging event link-status use-global switchport access vlan 1 switchport trunk native vlan 1 switchport trunk allowed vlan 1-4094 switchport mode access switchport mac address learning no switchport private-vlan mapping switchport default encapsulation dot1q vlan no l2-protocol encapsulation dot1q vlan 0 snmp trap link-status no switchport port-security switchport port-security maximum 1 sflow enable no spanning-tree portfast spanning-tree portfast auto no spanning-tree link-type no spanning-tree bpduguard no spanning-tree bpdufilter no spanning-tree cost spanning-tree port-priority 128 no spanning-tree guard no spanning-tree bpduguard rate-limit logging event spanning-tree use-global switchport tap native vlan 1 no switchport tap identity switchport tap allowed vlan 1-4094 switchport tool allowed vlan 1-4094 no switchport tool identity no switchport tap truncation no switchport tool truncation no switchport tap default group no switchport tool group no switchport tool dot1q remove outer vxlan multicast-group 239.10.10.10 vxlan source-interface Loopback0 no vxlan controller-client vxlan udp-port 4789 vxlan vlan 10 vni 10 vxlan vlan 10 flood vtep 3.3.3.3 4.4.4.4 vxlan flood vtep 1.1.1.1 2.2.2.2 no vxlan vlan flood vtep no vxlan learn-restrict vtep no vxlan vlan learn-restrict vtep no vxlan multicast-group decap ! pyeapi-1.0.2/test/fixtures/show_interfaces.json0000644000076500000240000004643014447405743023070 0ustar dlyssenkostaff00000000000000[ { "interfaces": { "Ethernet1": { "autoNegotiate": "unknown", "bandwidth": 10000000000, "description": "Managed by Ansible", "duplex": "duplexFull", "forwardingModel": "routed", "hardware": "ethernet", "interfaceAddress": [ { "broadcastAddress": "255.255.255.255", "primaryIp": { "address": "172.16.11.1", "maskLen": 24 }, "secondaryIps": {}, "secondaryIpsOrderedList": [], "virtualIp": { "address": "0.0.0.0", "maskLen": 0 } } ], "interfaceCounters": { "counterRefreshTime": 1410974362.64, "inBroadcastPkts": 425, "inDiscards": 0, "inMulticastPkts": 2553, "inOctets": 582780, "inUcastPkts": 0, "inputErrorsDetail": { "alignmentErrors": 0, "fcsErrors": 0, "giantFrames": 0, "runtFrames": 0, "rxPause": 0, "symbolErrors": 0 }, "linkStatusChanges": 1, "outBroadcastPkts": 102, "outDiscards": 0, "outMulticastPkts": 8250, "outOctets": 1231864, "outUcastPkts": 0, "outputErrorsDetail": { "collisions": 0, "deferredTransmissions": 0, "lateCollisions": 0, "txPause": 0 }, "totalInErrors": 0, "totalOutErrors": 0 }, "interfaceStatistics": { "inBitsRate": 0.0, "inPktsRate": 0.0, "outBitsRate": 0.0, "outPktsRate": 0.0, "updateInterval": 300.0 }, "interfaceStatus": "connected", "lastStatusChangeTimestamp": 1410901662.63, "lineProtocolStatus": "up", "mtu": 1500, "name": "Ethernet1", "physicalAddress": "00:0c:29:f5:d2:7d" }, "Ethernet2": { "autoNegotiate": "unknown", "bandwidth": 10000000000, "description": "Managed by Ansible", "duplex": "duplexFull", "forwardingModel": "routed", "hardware": "ethernet", "interfaceAddress": [ { "broadcastAddress": "255.255.255.255", "primaryIp": { "address": "172.16.12.1", "maskLen": 24 }, "secondaryIps": {}, "secondaryIpsOrderedList": [], "virtualIp": { "address": "0.0.0.0", "maskLen": 0 } } ], "interfaceCounters": { "counterRefreshTime": 1410974362.64, "inBroadcastPkts": 374, "inDiscards": 0, "inMulticastPkts": 2535, "inOctets": 572428, "inUcastPkts": 0, "inputErrorsDetail": { "alignmentErrors": 0, "fcsErrors": 0, "giantFrames": 0, "runtFrames": 0, "rxPause": 0, "symbolErrors": 0 }, "linkStatusChanges": 1, "outBroadcastPkts": 153, "outDiscards": 0, "outMulticastPkts": 8265, "outOctets": 1241847, "outUcastPkts": 0, "outputErrorsDetail": { "collisions": 0, "deferredTransmissions": 0, "lateCollisions": 0, "txPause": 0 }, "totalInErrors": 0, "totalOutErrors": 0 }, "interfaceStatistics": { "inBitsRate": 0.0, "inPktsRate": 0.0, "outBitsRate": 0.0, "outPktsRate": 0.0, "updateInterval": 300.0 }, "interfaceStatus": "connected", "lastStatusChangeTimestamp": 1410901662.63, "lineProtocolStatus": "up", "mtu": 1500, "name": "Ethernet2", "physicalAddress": "00:0c:29:f5:d2:7d" }, "Ethernet3": { "autoNegotiate": "off", "bandwidth": 10000000000, "burnedInAddress": "00:50:56:27:98:44", "description": "Managed by Ansible", "duplex": "duplexFull", "forwardingModel": "bridged", "hardware": "ethernet", "interfaceAddress": [], "interfaceCounters": { "counterRefreshTime": 1410974362.65, "inBroadcastPkts": 0, "inDiscards": 0, "inMulticastPkts": 0, "inOctets": 0, "inUcastPkts": 0, "inputErrorsDetail": { "alignmentErrors": 0, "fcsErrors": 0, "giantFrames": 0, "runtFrames": 0, "rxPause": 0, "symbolErrors": 0 }, "linkStatusChanges": 2, "outBroadcastPkts": 255, "outDiscards": 0, "outMulticastPkts": 6250, "outOctets": 835182, "outUcastPkts": 0, "outputErrorsDetail": { "collisions": 0, "deferredTransmissions": 0, "lateCollisions": 0, "txPause": 0 }, "totalInErrors": 0, "totalOutErrors": 0 }, "interfaceStatistics": { "inBitsRate": 0.0, "inPktsRate": 0.0, "outBitsRate": 0.0, "outPktsRate": 0.0, "updateInterval": 300.0 }, "interfaceStatus": "disabled", "lastStatusChangeTimestamp": 1410913331.7, "lineProtocolStatus": "down", "mtu": 9214, "name": "Ethernet3", "physicalAddress": "00:50:56:27:98:44" }, "Ethernet4": { "autoNegotiate": "off", "bandwidth": 10000000000, "burnedInAddress": "00:50:56:2d:19:03", "description": "Managed by Ansible", "duplex": "duplexFull", "forwardingModel": "bridged", "hardware": "ethernet", "interfaceAddress": [], "interfaceCounters": { "counterRefreshTime": 1410974362.66, "inBroadcastPkts": 0, "inDiscards": 0, "inMulticastPkts": 0, "inOctets": 0, "inUcastPkts": 0, "inputErrorsDetail": { "alignmentErrors": 0, "fcsErrors": 0, "giantFrames": 0, "runtFrames": 0, "rxPause": 0, "symbolErrors": 0 }, "lastClear": 1410901491.4, "linkStatusChanges": 2, "outBroadcastPkts": 255, "outDiscards": 0, "outMulticastPkts": 6250, "outOctets": 835182, "outUcastPkts": 0, "outputErrorsDetail": { "collisions": 0, "deferredTransmissions": 0, "lateCollisions": 0, "txPause": 0 }, "totalInErrors": 0, "totalOutErrors": 0 }, "interfaceStatistics": { "inBitsRate": 0.0, "inPktsRate": 0.0, "outBitsRate": 0.0, "outPktsRate": 0.0, "updateInterval": 300.0 }, "interfaceStatus": "disabled", "lastStatusChangeTimestamp": 1410913332.36, "lineProtocolStatus": "down", "mtu": 9214, "name": "Ethernet4", "physicalAddress": "00:50:56:2d:19:03" }, "Ethernet5": { "autoNegotiate": "off", "bandwidth": 10000000000, "burnedInAddress": "00:0c:29:2a:74:36", "description": "Managed by Ansible", "duplex": "duplexFull", "forwardingModel": "bridged", "hardware": "ethernet", "interfaceAddress": [], "interfaceCounters": { "counterRefreshTime": 1410974362.66, "inBroadcastPkts": 0, "inDiscards": 0, "inMulticastPkts": 0, "inOctets": 0, "inUcastPkts": 0, "inputErrorsDetail": { "alignmentErrors": 0, "fcsErrors": 0, "giantFrames": 0, "runtFrames": 0, "rxPause": 0, "symbolErrors": 0 }, "linkStatusChanges": 2, "outBroadcastPkts": 255, "outDiscards": 0, "outMulticastPkts": 6251, "outOctets": 835305, "outUcastPkts": 0, "outputErrorsDetail": { "collisions": 0, "deferredTransmissions": 0, "lateCollisions": 0, "txPause": 0 }, "totalInErrors": 0, "totalOutErrors": 0 }, "interfaceStatistics": { "inBitsRate": 0.0, "inPktsRate": 0.0, "outBitsRate": 0.0, "outPktsRate": 0.0, "updateInterval": 300.0 }, "interfaceStatus": "disabled", "lastStatusChangeTimestamp": 1410913332.94, "lineProtocolStatus": "down", "mtu": 9214, "name": "Ethernet5", "physicalAddress": "00:0c:29:2a:74:36" }, "Ethernet6": { "autoNegotiate": "off", "bandwidth": 10000000000, "burnedInAddress": "00:0c:29:2a:74:40", "description": "Managed by Ansible", "duplex": "duplexFull", "forwardingModel": "bridged", "hardware": "ethernet", "interfaceAddress": [], "interfaceCounters": { "counterRefreshTime": 1410974362.67, "inBroadcastPkts": 0, "inDiscards": 0, "inMulticastPkts": 0, "inOctets": 0, "inUcastPkts": 0, "inputErrorsDetail": { "alignmentErrors": 0, "fcsErrors": 0, "giantFrames": 0, "runtFrames": 0, "rxPause": 0, "symbolErrors": 0 }, "linkStatusChanges": 2, "outBroadcastPkts": 255, "outDiscards": 0, "outMulticastPkts": 6251, "outOctets": 835305, "outUcastPkts": 0, "outputErrorsDetail": { "collisions": 0, "deferredTransmissions": 0, "lateCollisions": 0, "txPause": 0 }, "totalInErrors": 0, "totalOutErrors": 0 }, "interfaceStatistics": { "inBitsRate": 0.0, "inPktsRate": 0.0, "outBitsRate": 0.0, "outPktsRate": 0.0, "updateInterval": 300.0 }, "interfaceStatus": "disabled", "lastStatusChangeTimestamp": 1410913333.57, "lineProtocolStatus": "down", "mtu": 9214, "name": "Ethernet6", "physicalAddress": "00:0c:29:2a:74:40" }, "Ethernet7": { "autoNegotiate": "off", "bandwidth": 10000000000, "burnedInAddress": "00:0c:29:2a:74:4a", "description": "Managed by Ansible", "duplex": "duplexFull", "forwardingModel": "bridged", "hardware": "ethernet", "interfaceAddress": [], "interfaceCounters": { "counterRefreshTime": 1410974362.67, "inBroadcastPkts": 0, "inDiscards": 0, "inMulticastPkts": 0, "inOctets": 0, "inUcastPkts": 0, "inputErrorsDetail": { "alignmentErrors": 0, "fcsErrors": 0, "giantFrames": 0, "runtFrames": 0, "rxPause": 0, "symbolErrors": 0 }, "linkStatusChanges": 2, "outBroadcastPkts": 255, "outDiscards": 0, "outMulticastPkts": 6251, "outOctets": 835305, "outUcastPkts": 0, "outputErrorsDetail": { "collisions": 0, "deferredTransmissions": 0, "lateCollisions": 0, "txPause": 0 }, "totalInErrors": 0, "totalOutErrors": 0 }, "interfaceStatistics": { "inBitsRate": 0.0, "inPktsRate": 0.0, "outBitsRate": 0.0, "outPktsRate": 0.0, "updateInterval": 300.0 }, "interfaceStatus": "disabled", "lastStatusChangeTimestamp": 1410913334.23, "lineProtocolStatus": "down", "mtu": 9214, "name": "Ethernet7", "physicalAddress": "00:0c:29:2a:74:4a" }, "Loopback0": { "bandwidth": 0, "description": "Managed by Ansible", "forwardingModel": "routed", "hardware": "loopback", "interfaceAddress": [ { "broadcastAddress": "255.255.255.255", "primaryIp": { "address": "1.1.1.1", "maskLen": 32 }, "secondaryIps": {}, "secondaryIpsOrderedList": [], "virtualIp": { "address": "0.0.0.0", "maskLen": 0 } } ], "interfaceStatus": "connected", "lastStatusChangeTimestamp": 1410913334.97, "lineProtocolStatus": "up", "mtu": 65535, "name": "Loopback0" }, "Management1": { "autoNegotiate": "success", "bandwidth": 1000000000, "burnedInAddress": "00:1c:73:2a:00:01", "description": "", "duplex": "duplexFull", "forwardingModel": "routed", "hardware": "ethernet", "interfaceAddress": [ { "broadcastAddress": "255.255.255.255", "primaryIp": { "address": "192.168.1.16", "maskLen": 24 }, "secondaryIps": {}, "secondaryIpsOrderedList": [], "virtualIp": { "address": "0.0.0.0", "maskLen": 0 } } ], "interfaceCounters": { "counterRefreshTime": 1410974362.68, "inBroadcastPkts": 0, "inDiscards": 0, "inMulticastPkts": 0, "inOctets": 15172431, "inUcastPkts": 0, "inputErrorsDetail": { "alignmentErrors": 0, "fcsErrors": 0, "giantFrames": 0, "runtFrames": 0, "rxPause": 0, "symbolErrors": 0 }, "linkStatusChanges": 3, "outBroadcastPkts": 0, "outDiscards": 0, "outMulticastPkts": 0, "outOctets": 821068, "outUcastPkts": 7823, "outputErrorsDetail": { "collisions": 0, "deferredTransmissions": 0, "lateCollisions": 0, "txPause": 0 }, "totalInErrors": 0, "totalOutErrors": 0 }, "interfaceStatistics": { "inBitsRate": 1679.52284441, "inPktsRate": 0.0, "outBitsRate": 65.2084066523, "outPktsRate": 0.0837097191065, "updateInterval": 300.0 }, "interfaceStatus": "connected", "lastStatusChangeTimestamp": 1410901662.74, "lineProtocolStatus": "up", "mtu": 1500, "name": "Management1", "physicalAddress": "00:1c:73:2a:00:01" } } } ] pyeapi-1.0.2/test/fixtures/show_interfaces.text0000644000076500000240000001602114447405743023074 0ustar dlyssenkostaff00000000000000{"output": "Ethernet1 is up, line protocol is up (connected)\n Hardware is Ethernet, address is 0050.563f.0c0b (bia 0050.563f.0c0b)\n Ethernet MTU 9214 bytes , BW 10000000 kbit\n Full-duplex, 10Gb/s, auto negotiation: off, uni-link: unknown\n Up 4 minutes, 24 seconds\n 1 link status changes since last clear\n Last clearing of \"show interface\" counters never\n 5 minutes input rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 5 minutes output rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 0 packets input, 0 bytes\n Received 0 broadcasts, 0 multicast\n 0 runts, 0 giants\n 0 input errors, 0 CRC, 0 alignment, 0 symbol, 0 input discards\n 0 PAUSE input\n 173 packets output, 19867 bytes\n Sent 34 broadcasts, 139 multicast\n 0 output errors, 0 collisions\n 0 late collision, 0 deferred, 0 output discards\n 0 PAUSE output\nEthernet2 is up, line protocol is up (connected)\n Hardware is Ethernet, address is 0050.563f.dbec (bia 0050.563f.dbec)\n Ethernet MTU 9214 bytes , BW 10000000 kbit\n Full-duplex, 10Gb/s, auto negotiation: off, uni-link: unknown\n Up 4 minutes, 24 seconds\n 1 link status changes since last clear\n Last clearing of \"show interface\" counters never\n 5 minutes input rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 5 minutes output rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 69 packets input, 6068 bytes\n Received 51 broadcasts, 10 multicast\n 0 runts, 0 giants\n 0 input errors, 0 CRC, 0 alignment, 0 symbol, 0 input discards\n 0 PAUSE input\n 139 packets output, 17691 bytes\n Sent 0 broadcasts, 139 multicast\n 0 output errors, 0 collisions\n 0 late collision, 0 deferred, 0 output discards\n 0 PAUSE output\nEthernet3 is up, line protocol is up (connected)\n Hardware is Ethernet, address is 0050.5627.9844 (bia 0050.5627.9844)\n Ethernet MTU 9214 bytes , BW 10000000 kbit\n Full-duplex, 10Gb/s, auto negotiation: off, uni-link: unknown\n Up 4 minutes, 24 seconds\n 1 link status changes since last clear\n Last clearing of \"show interface\" counters never\n 5 minutes input rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 5 minutes output rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 0 packets input, 0 bytes\n Received 0 broadcasts, 0 multicast\n 0 runts, 0 giants\n 0 input errors, 0 CRC, 0 alignment, 0 symbol, 0 input discards\n 0 PAUSE input\n 173 packets output, 19867 bytes\n Sent 34 broadcasts, 139 multicast\n 0 output errors, 0 collisions\n 0 late collision, 0 deferred, 0 output discards\n 0 PAUSE output\nEthernet4 is up, line protocol is up (connected)\n Hardware is Ethernet, address is 0050.562d.1903 (bia 0050.562d.1903)\n Ethernet MTU 9214 bytes , BW 10000000 kbit\n Full-duplex, 10Gb/s, auto negotiation: off, uni-link: unknown\n Up 4 minutes, 24 seconds\n 1 link status changes since last clear\n Last clearing of \"show interface\" counters 0:05:52 ago\n 5 minutes input rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 5 minutes output rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 0 packets input, 0 bytes\n Received 0 broadcasts, 0 multicast\n 0 runts, 0 giants\n 0 input errors, 0 CRC, 0 alignment, 0 symbol, 0 input discards\n 0 PAUSE input\n 173 packets output, 19867 bytes\n Sent 34 broadcasts, 139 multicast\n 0 output errors, 0 collisions\n 0 late collision, 0 deferred, 0 output discards\n 0 PAUSE output\nEthernet5 is up, line protocol is up (connected)\n Hardware is Ethernet, address is 000c.292a.7436 (bia 000c.292a.7436)\n Ethernet MTU 9214 bytes , BW 10000000 kbit\n Full-duplex, 10Gb/s, auto negotiation: off, uni-link: unknown\n Up 4 minutes, 24 seconds\n 1 link status changes since last clear\n Last clearing of \"show interface\" counters never\n 5 minutes input rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 5 minutes output rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 0 packets input, 0 bytes\n Received 0 broadcasts, 0 multicast\n 0 runts, 0 giants\n 0 input errors, 0 CRC, 0 alignment, 0 symbol, 0 input discards\n 0 PAUSE input\n 173 packets output, 19867 bytes\n Sent 34 broadcasts, 139 multicast\n 0 output errors, 0 collisions\n 0 late collision, 0 deferred, 0 output discards\n 0 PAUSE output\nEthernet6 is up, line protocol is up (connected)\n Hardware is Ethernet, address is 000c.292a.7440 (bia 000c.292a.7440)\n Ethernet MTU 9214 bytes , BW 10000000 kbit\n Full-duplex, 10Gb/s, auto negotiation: off, uni-link: unknown\n Up 4 minutes, 24 seconds\n 1 link status changes since last clear\n Last clearing of \"show interface\" counters never\n 5 minutes input rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 5 minutes output rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 0 packets input, 0 bytes\n Received 0 broadcasts, 0 multicast\n 0 runts, 0 giants\n 0 input errors, 0 CRC, 0 alignment, 0 symbol, 0 input discards\n 0 PAUSE input\n 173 packets output, 19867 bytes\n Sent 34 broadcasts, 139 multicast\n 0 output errors, 0 collisions\n 0 late collision, 0 deferred, 0 output discards\n 0 PAUSE output\nEthernet7 is up, line protocol is up (connected)\n Hardware is Ethernet, address is 000c.292a.744a (bia 000c.292a.744a)\n Ethernet MTU 9214 bytes , BW 10000000 kbit\n Full-duplex, 10Gb/s, auto negotiation: off, uni-link: unknown\n Up 4 minutes, 24 seconds\n 1 link status changes since last clear\n Last clearing of \"show interface\" counters never\n 5 minutes input rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 5 minutes output rate 0 bps (0.0% with framing overhead), 0 packets/sec\n 0 packets input, 0 bytes\n Received 0 broadcasts, 0 multicast\n 0 runts, 0 giants\n 0 input errors, 0 CRC, 0 alignment, 0 symbol, 0 input discards\n 0 PAUSE input\n 173 packets output, 19867 bytes\n Sent 34 broadcasts, 139 multicast\n 0 output errors, 0 collisions\n 0 late collision, 0 deferred, 0 output discards\n 0 PAUSE output\nManagement1 is up, line protocol is up (connected)\n Hardware is Ethernet, address is 001c.732a.0001 (bia 001c.732a.0001)\n Internet address is 192.168.10.16/24\n Broadcast address is 255.255.255.255\n Address determined by manual configuration\n IP MTU 1500 bytes , BW 1000000 kbit\n Full-duplex, 1Gb/s, auto negotiation: on, uni-link: unknown\n Up 4 minutes, 31 seconds\n 3 link status changes since last clear\n Last clearing of \"show interface\" counters never\n 5 minutes input rate 57 bps (0.0% with framing overhead), 0 packets/sec\n 5 minutes output rate 73 bps (0.0% with framing overhead), 0 packets/sec\n 0 packets input, 2775 bytes\n Received 0 broadcasts, 0 multicast\n 0 runts, 0 giants\n 0 input errors, 0 CRC, 0 alignment, 0 symbol, 0 input discards\n 0 PAUSE input\n 62 packets output, 4267 bytes\n Sent 0 broadcasts, 0 multicast\n 0 output errors, 0 collisions\n 0 late collision, 0 deferred, 0 output discards\n 0 PAUSE output\n"} pyeapi-1.0.2/test/fixtures/show_portchannel.json0000644000076500000240000000027714447405743023261 0ustar dlyssenkostaff00000000000000[{ "command": "show port-channel 1 all-ports", "result": { "output": "Port Channel Port-Channel1:\n Active Ports: Ethernet5 Ethernet6 \n" }, "encoding": "text" }] pyeapi-1.0.2/test/fixtures/show_version.json0000644000076500000240000000071214447405743022423 0ustar dlyssenkostaff00000000000000{ "architecture": "i386", "bootupTimestamp": 1410471963.75, "hardwareRevision": "01.07", "internalBuildId": "7a4a170a-38f1-43eb-95df-53ec81fc61e1", "internalVersion": "4.14.1-2055159.fldaytonamplspush", "memFree": 1318624, "memTotal": 3992600, "modelName": "DCS-7048T-A-F", "serialNumber": "JPE14151521", "systemMacAddress": "00:1c:73:61:79:79", "version": "4.14.1-2055159.fldaytonamplspush (engineering build)" } pyeapi-1.0.2/test/fixtures/show_version.text0000644000076500000240000000062214447405743022436 0ustar dlyssenkostaff00000000000000{"output": "Arista vEOS\nHardware version: \nSerial number: \nSystem MAC address: 000c.29f5.d27d\n\nSoftware image version: 4.14.0F\nArchitecture: i386\nInternal build version: 4.14.0F-1984100.flbocaveos.1\nInternal build ID: adac661f-0826-4894-bd9d-524c719798b0\n\nUptime: 5 minutes\nTotal memory: 2028128 kB\nFree memory: 448440 kB\n\n"} pyeapi-1.0.2/test/fixtures/vxlan.json0000644000076500000240000000125514447405743021031 0ustar dlyssenkostaff00000000000000[ { "interfaces": { "Vxlan1": { "vniInDottedNotation": false, "name": "Vxlan1", "interfaceStatus": "connected", "description": "", "vlanToVtepList": {}, "mtu": 0, "hardware": "vxlan", "mcastGrpDecap": "", "replicationMode": "multicast", "bandwidth": 0, "floodMcastGrp": "239.10.10.10", "vlanToVniMap": {}, "srcIpIntf": "Loopback0", "srcIpAddr": "1.1.1.1", "interfaceAddress": [], "lineProtocolStatus": "up", "forwardingModel": "bridged" } } } ] pyeapi-1.0.2/test/lib/0000755000076500000240000000000014447406213015671 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/test/lib/systestlib.py0000644000076500000240000000552014447405743020461 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import unittest import random from testlib import get_fixture import pyeapi.client class DutSystemTest(unittest.TestCase): def __init__(self, *args, **kwargs): super(DutSystemTest, self).__init__(*args, **kwargs) self.longMessage = True def setUp(self): pyeapi.client.load_config(filename=get_fixture('dut.conf')) config = pyeapi.client.config self.duts = list() for name in config.sections(): if name.startswith('connection:') and 'localhost' not in name: name = name.split(':')[1] self.duts.append(pyeapi.client.connect_to(name)) def sort_dict_by_keys(self, d): keys = sorted(d.keys()) return dict([(k, d[k]) for k in keys]) def random_interface(dut, exclude=None): exclude = [] if exclude is None else exclude interfaces = dut.api('interfaces') names = [name for name in list(interfaces.keys()) if name.startswith('Et')] exclude_interfaces = dut.settings.get('exclude_interfaces', []) if exclude_interfaces: exclude_interfaces = exclude_interfaces.split(',') exclude_interfaces.extend(exclude) if sorted(exclude_interfaces) == sorted(names): raise TypeError('unable to allocate interface from dut') choices = set(names).difference(exclude) return random.choice(list(choices)) pyeapi-1.0.2/test/lib/testlib.py0000644000076500000240000001131614447405743017722 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import random import string import unittest from unittest.mock import MagicMock as Mock from pyeapi.utils import CliVariants from pyeapi.client import Node def get_fixtures_path(): return os.path.join(os.path.dirname(__file__), '../fixtures') def get_fixture(filename): return os.path.join(get_fixtures_path(), filename) def random_string(minchar=1, maxchar=50): return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(random.randint(minchar, maxchar))) def random_vlan(): return random.randint(1, 4094) def random_int(minvalue, maxvalue): return random.randint(minvalue, maxvalue) from collections import namedtuple Function = namedtuple('Function', 'name args kwargs') def function(name, *args, **kwargs): return Function(name, args, kwargs) class EapiConfigUnitTest(unittest.TestCase): def __init__(self, *args, **kwargs): self.instance = None self.config = None super(EapiConfigUnitTest, self).__init__(*args, **kwargs) def setUp(self): self.node = Node(None) self.node._version_number = '4.17.1.1' self.node._running_config = self.config self.mock_config = Mock(name='node.config') self.node.config = self.mock_config self.mock_enable = Mock(name='node.enable') self.node.enable = self.mock_enable self.assertIsNotNone(self.instance) self.instance.node = self.node def eapi_config_test(self, func, cmds=None, *args, **kwargs): func, fargs, fkwargs = func func = getattr(self.instance, func) if cmds is not None: lcmds = len([cmds]) if isinstance(cmds, str) else len(cmds) self.mock_config.return_value = [{} for i in range(0, lcmds)] result = func(*fargs, **fkwargs) if cmds is not None: # if config was called with CliVariants, then create all possible # cli combinations with CliVariants and see if cmds is one of them called_args = list( self.node.config.call_args )[ 0 ][ 0 ] variants = [ x for x in called_args if isinstance(x, CliVariants) ] if not variants: self.node.config.assert_called_with(cmds) return result # process all variants cli_variants = CliVariants.expand( called_args ) self.assertIn( cmds, cli_variants ) else: self.assertEqual(self.node.config.call_count, 0) return result def eapi_positive_config_test(self, func, cmds=None, *args, **kwargs): result = self.eapi_config_test(func, cmds, *args, **kwargs) self.assertTrue(result) def eapi_negative_config_test(self, func, cmds=None, *args, **kwargs): result = self.eapi_config_test(func, cmds, *args, **kwargs) self.assertFalse(result) def eapi_exception_config_test(self, func, exc, *args, **kwargs): with self.assertRaises(exc): self.eapi_config_test(func, *args, **kwargs) def eapi_positive_config_with_input_test(self, func, cmds=None, *args, **kwargs): result = self.eapi_config_test(func, cmds, *args, **kwargs) self.assertTrue(result) pyeapi-1.0.2/test/system/0000755000076500000240000000000014447406213016447 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/test/system/test_api_acl.py0000644000076500000240000002412614447405743021464 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest class TestApiStandardAcls(DutSystemTest): def test_get(self): for dut in self.duts: dut.config(['no ip access-list standard test', 'ip access-list standard test']) response = dut.api('acl').get('test') self.assertIsNotNone(response) def test_get_none(self): for dut in self.duts: dut.config('no ip access-list standard test') result = dut.api('acl').get('test') self.assertIsNone(result) def test_getall(self): for dut in self.duts: dut.config(['no ip access-list standard test', 'ip access-list standard test']) result = dut.api('acl').getall() self.assertIn('test', result['standard']) def test_create(self): for dut in self.duts: dut.config('no ip access-list standard test') api = dut.api('acl') self.assertIsNone(api.get('test')) result = dut.api('acl').create('test') self.assertTrue(result) self.assertIsNotNone(api.get('test')) def test_delete(self): for dut in self.duts: dut.config('ip access-list standard test') api = dut.api('acl') self.assertIsNotNone(api.get('test')) result = dut.api('acl').delete('test') self.assertTrue(result) self.assertIsNone(api.get('test')) def test_default(self): for dut in self.duts: dut.config('ip access-list standard test') api = dut.api('acl') self.assertIsNotNone(api.get('test')) result = dut.api('acl').default('test') self.assertTrue(result) self.assertIsNone(api.get('test')) def test_update_entry(self): for dut in self.duts: dut.config(['no ip access-list standard test', 'ip access-list standard test']) api = dut.api('acl') self.assertNotIn('10 permit any log', api.get_block('ip access-list standard test')) result = dut.api('acl').update_entry('test', '10', 'permit', '0.0.0.0', '0', True) self.assertTrue(result) self.assertIn('10 permit any log', api.get_block('ip access-list standard test')) def test_update_entry_existing(self): for dut in self.duts: dut.config(['no ip access-list standard test', 'ip access-list standard test', '10 permit any log']) api = dut.api('acl') self.assertIn('10 permit any log', api.get_block('ip access-list standard test')) result = dut.api('acl').update_entry('test', '10', 'deny', '0.0.0.0', '0', True) self.assertTrue(result) self.assertIn('10 deny any log', api.get_block('ip access-list standard test')) def test_add_entry(self): for dut in self.duts: dut.config(['no ip access-list standard test', 'ip access-list standard test']) api = dut.api('acl') self.assertNotIn('10 permit any log', api.get_block('ip access-list standard test')) result = api.add_entry('test', 'permit', '0.0.0.0', '0', True) self.assertTrue(result) self.assertIn('10 permit any log', api.get_block('ip access-list standard test')) def test_remove_entry(self): for dut in self.duts: dut.config(['no ip access-list standard test', 'ip access-list standard test', '10 permit any log']) api = dut.api('acl') self.assertIn('10 permit any log', api.get_block('ip access-list standard test')) result = api.remove_entry('test', '10') self.assertTrue(result) self.assertNotIn('10 permit any log', api.get_block('ip access-list standard test')) class TestApiExtendedAcls(DutSystemTest): def test_get(self): for dut in self.duts: dut.config(['no ip access-list exttest', 'ip access-list exttest']) response = dut.api('acl').get('exttest') self.assertIsNotNone(response) def test_get_none(self): for dut in self.duts: dut.config('no ip access-list exttest') result = dut.api('acl').get('exttest') self.assertIsNone(result) def test_getall(self): for dut in self.duts: dut.config(['no ip access-list exttest', 'ip access-list exttest']) result = dut.api('acl').getall() self.assertIn('exttest', result['extended']) def test_create(self): for dut in self.duts: dut.config('no ip access-list exttest') api = dut.api('acl') self.assertIsNone(api.get('exttest')) result = dut.api('acl').create('exttest', 'extended') self.assertTrue(result) self.assertIsNotNone(api.get('exttest')) def test_delete(self): for dut in self.duts: dut.config('ip access-list exttest') api = dut.api('acl') self.assertIsNotNone(api.get('exttest')) result = dut.api('acl').delete('exttest') self.assertTrue(result) self.assertIsNone(api.get('exttest')) def test_default(self): for dut in self.duts: dut.config('ip access-list exttest') api = dut.api('acl') self.assertIsNotNone(api.get('exttest')) result = dut.api('acl').default('exttest') self.assertTrue(result) self.assertIsNone(api.get('exttest')) def test_update_entry(self): for dut in self.duts: dut.config(['no ip access-list exttest', 'ip access-list exttest']) api = dut.api('acl') self.assertNotIn('10 permit ip any any', api.get_block('ip access-list exttest')) result = dut.api('acl').update_entry('exttest', '10', 'permit', 'ip', '0.0.0.0', '0', '0.0.0.0', '0', False) self.assertTrue(result) self.assertIn('10 permit ip any any', api.get_block('ip access-list exttest')) def test_update_entry_existing(self): for dut in self.duts: dut.config(['no ip access-list exttest', 'ip access-list exttest', '10 permit ip any any log']) api = dut.api('acl') self.assertIn('10 permit ip any any log', api.get_block('ip access-list exttest')) result = dut.api('acl').update_entry('exttest', '10', 'deny', 'ip', '0.0.0.0', '0', '0.0.0.0', '0', True) self.assertTrue(result) self.assertIn('10 deny ip any any log', api.get_block('ip access-list exttest')) def test_add_entry(self): for dut in self.duts: dut.config(['no ip access-list exttest', 'ip access-list exttest']) api = dut.api('acl') self.assertNotIn('10 permit ip any any log', api.get_block('ip access-list exttest')) result = api.add_entry('exttest', 'permit', 'ip', '0.0.0.0', '0', '0.0.0.0', '0', True) self.assertTrue(result) self.assertIn('10 permit ip any any log', api.get_block('ip access-list exttest')) def test_remove_entry(self): for dut in self.duts: dut.config(['no ip access-list exttest', 'ip access-list exttest', '10 permit ip any any log']) api = dut.api('acl') self.assertIn('10 permit ip any any log', api.get_block('ip access-list exttest')) result = api.remove_entry('exttest', '10') self.assertTrue(result) self.assertNotIn('10 permit ip any any log', api.get_block('ip access-list exttest')) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_interfaces.py0000644000076500000240000007252214447405743023053 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest from pyeapi.utils import CliVariants import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import random_string, random_int from systestlib import DutSystemTest, random_interface from time import sleep class TestResourceInterfaces(DutSystemTest): def test_get(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'description this is a test', 'flowcontrol send off', 'flowcontrol receive on', 'no sflow enable']) result = dut.api('interfaces').get(intf) self.assertIsInstance(result, dict) self.assertEqual(result['description'], 'this is a test') self.assertFalse(result['shutdown']) self.assertEqual(result['flowcontrol_send'], 'off') self.assertEqual(result['flowcontrol_receive'], 'on') self.assertFalse(result['sflow']) def test_getall(self): for dut in self.duts: rintf = random_interface(dut) dut.config(['default interface %s' % rintf]) result = dut.api('interfaces').getall() self.assertIsInstance(result, dict) for intf in [rintf, 'Management1']: self.assertIn(intf, result) def test_create_and_return_true(self): for dut in self.duts: dut.config('no interface Loopback0') result = dut.api('interfaces').create('Loopback0') self.assertTrue(result) config = dut.run_commands('show interfaces') self.assertIn('Loopback0', config[0]['interfaces']) def test_create_ethernet_raises_not_implemented_error(self): for dut in self.duts: with self.assertRaises(NotImplementedError): dut.api('interfaces').create(random_interface(dut)) def test_delete_and_return_true(self): for dut in self.duts: dut.config('interface Loopback0') result = dut.api('interfaces').delete('Loopback0') self.assertTrue(result) config = dut.run_commands('show interfaces') self.assertNotIn('Loopback0', config[0]['interfaces']) def test_delete_ethernet_raises_not_implemented_error(self): for dut in self.duts: with self.assertRaises(NotImplementedError): dut.api('interfaces').delete(random_interface(dut)) def test_create_and_delete_ethernet_sub_interface(self): for dut in self.duts: # Default Ethernet1 dut.api('interfaces').default('Ethernet1') # Create subint Ethernet1.1 res = dut.api('interfaces').create('Ethernet1.1') self.assertTrue(res) command = 'show running-config interfaces Ethernet1.1' output = dut.run_commands(command, encoding='text') self.assertIn('Ethernet1.1', output[0]['output']) # Delete subint Ethernet1.1 res = dut.api('interfaces').delete('Ethernet1.1') self.assertTrue(res) output = dut.run_commands(command, encoding='text') self.assertEqual(output[0]['output'], '') def test_ethernet_set_and_unset_encapsulation(self): for dut in self.duts: # Default Ethernet1 dut.api('interfaces').default('Ethernet1') # Create subint Ethernet1.1 res = dut.api('interfaces').create('Ethernet1.1') self.assertTrue(res) # Set encapsulation res = dut.api('interfaces').set_encapsulation('Ethernet1.1', 4) self.assertTrue(res) command = 'show running-config interfaces Ethernet1.1' output = dut.run_commands(command, encoding='text') encap = 'encapsulation dot1q vlan 4' self.assertIn(encap, output[0]['output']) # Remove encapsulation res = dut.api('interfaces').set_encapsulation('Ethernet1.1', 4, disable=True) self.assertTrue(res) output = dut.run_commands(command, encoding='text') self.assertNotIn(encap, output[0]['output']) # Delete subint Ethernet1.1 res = dut.api('interfaces').delete('Ethernet1.1') self.assertTrue(res) def test_set_encapsulation_non_subintf_exception(self): for dut in self.duts: with self.assertRaises(NotImplementedError): dut.api('interfaces').set_encapsulation(random_interface(dut), 1) def test_set_encapsulation_non_supported_intf_exception(self): for dut in self.duts: with self.assertRaises(NotImplementedError): dut.api('interfaces').set_encapsulation('Vlan1234', 1) def test_default(self): for dut in self.duts: intf = random_interface(dut) intf_status = dut.run_commands('show interfaces %s' % intf) intf_status = intf_status[0]['interfaces'][intf]['interfaceStatus'] dut.config(['interface %s' % intf, 'shutdown']) result = dut.api('interfaces').default(intf) sleep( 10 ) # if intf was 'connected', give it a time to come up self.assertTrue(result) config = dut.run_commands('show interfaces %s' % intf) config = config[0]['interfaces'][intf] self.assertEqual(config['interfaceStatus'], intf_status) def test_set_description(self): for dut in self.duts: text = random_string() intf = random_interface(dut) result = dut.api('interfaces').set_description(intf, text) self.assertTrue(result) config = dut.run_commands('show interfaces %s' % intf) config = config[0]['interfaces'][intf] self.assertEqual(config['description'], text) def test_set_description_negate(self): for dut in self.duts: text = random_string() intf = random_interface(dut) dut.config(['interface %s' % intf, 'description %s' % text]) result = dut.api('interfaces').set_description(intf, disable=True) self.assertTrue(result) config = dut.run_commands('show interfaces %s' % intf) config = config[0]['interfaces'][intf] self.assertEqual(config['description'], '') def test_set_description_default(self): for dut in self.duts: text = random_string() intf = random_interface(dut) dut.config(['interface %s' % intf, 'description %s' % text]) result = dut.api('interfaces').set_description(intf, default=True) self.assertTrue(result) config = dut.run_commands('show interfaces %s' % intf) config = config[0]['interfaces'][intf] self.assertEqual(config['description'], '') def test_set_sflow_enable(self): for dut in self.duts: intf = random_interface(dut) dut.config(['interface %s' % intf, 'no sflow enable']) result = dut.api('interfaces').set_sflow(intf, True) self.assertTrue(result) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertNotIn('no sflow enable', config[0]['output']) def test_set_sflow_disable(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) result = dut.api('interfaces').set_sflow(intf, disable=True) self.assertTrue(result) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertIn('no sflow enable', config[0]['output']) def test_set_sflow_default(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) result = dut.api('interfaces').set_sflow(intf, default=True) self.assertTrue(result) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertNotIn('no sflow enable', config[0]['output']) def test_set_vrf(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) # Verify set_vrf returns False if no vrf by name is configured result = dut.api('interfaces').set_vrf(intf, 'test') self.assertFalse(result) dut.config( CliVariants('vrf instance test', 'vrf definition test') ) # Verify interface has vrf applied result = dut.api('interfaces').set_vrf(intf, 'test') self.assertTrue(result) config = dut.run_commands( f'show running-config interfaces {intf}', 'text' ) self.assertIn('vrf test' if dut.version_number >= '4.23' else 'vrf forwarding test', config[0]['output']) # Verify interface has vrf removed result = dut.api('interfaces').set_vrf(intf, 'test', disable=True) self.assertTrue(result) config = dut.run_commands( f'show running-config interfaces {intf}', 'text' ) self.assertNotIn('vrf test' if dut.version_number >= '4.23' else 'vrf forwarding test', config[0]['output']) dut.config( CliVariants( 'no vrf instance test', 'no vrf definition test') ) class TestPortchannelInterface(DutSystemTest): def test_get(self): for dut in self.duts: dut.config(['no interface Port-Channel1', 'interface Port-Channel1']) result = dut.api('interfaces').get('Port-Channel1') self.assertIsInstance(result, dict) self.assertEqual(result['type'], 'portchannel') self.assertEqual(result['name'], 'Port-Channel1') def test_set_members(self): for dut in self.duts: et1 = random_interface(dut) et2 = random_interface(dut, exclude=[et1]) et3 = random_interface(dut, exclude=[et1, et2]) dut.config(['no interface Port-Channel1', 'default interface %s' % et1, 'interface %s' % et1, 'channel-group 1 mode on', 'default interface %s' % et2, 'interface %s' % et2, 'channel-group 1 mode on', 'default interface %s' % et3]) api = dut.api('interfaces') result = api.set_members('Port-Channel1', [et1, et3]) self.assertTrue(result, 'dut=%s' % dut) cmd = 'show running-config interfaces %s' # check to make sure et1 is still in the lag and et3 was # added to the lag for interface in [et1, et3]: config = dut.run_commands(cmd % interface, 'text') self.assertIn('channel-group 1 mode on', config[0]['output'], 'dut=%s' % dut) # checks to make sure et2 was remvoved form the lag config = dut.run_commands(cmd % et2, 'text') self.assertNotIn('channel-group 1 mode on', config[0]['output'], 'dut=%s' % dut) def test_set_members_with_mode(self): for dut in self.duts: et1 = random_interface(dut) et2 = random_interface(dut, exclude=[et1]) et3 = random_interface(dut, exclude=[et1, et2]) dut.config(['no interface Port-Channel1', 'default interface %s' % et1, 'interface %s' % et1, 'channel-group 1 mode on', 'default interface %s' % et2, 'interface %s' % et2, 'channel-group 1 mode on', 'default interface %s' % et3]) api = dut.api('interfaces') result = api.set_members('Port-Channel1', [et1, et3], mode='active') self.assertTrue(result, 'dut=%s' % dut) cmd = 'show running-config interfaces %s' # check to make sure et1 is still in the lag and et3 was # added to the lag for interface in [et1, et3]: config = dut.run_commands(cmd % interface, 'text') self.assertIn('channel-group 1 mode active', config[0]['output'], 'dut=%s' % dut) # checks to make sure et2 was remvoved form the lag config = dut.run_commands(cmd % et2, 'text') self.assertNotIn('channel-group 1 mode on', config[0]['output'], 'dut=%s' % dut) def test_get_members_default(self): for dut in self.duts: dut.config(['no interface Port-Channel1', 'interface Port-Channel1']) instance = dut.api('interfaces').get_instance('Port-Channel1') result = instance.get_members('Port-Channel1') self.assertEqual(result, list(), 'dut=%s' % dut) def test_get_members_one_member(self): for dut in self.duts: dut.config(['no interface Port-Channel1', 'interface Port-Channel1', 'default interface Ethernet1', 'interface Ethernet1', 'channel-group 1 mode active']) instance = dut.api('interfaces').get_instance('Port-Channel1') result = instance.get_members('Port-Channel1') self.assertEqual(result, ['Ethernet1'], 'dut=%s' % dut) def test_get_members_two_members(self): for dut in self.duts: dut.config(['no interface Port-Channel1', 'interface Port-Channel1', 'default interface Ethernet1-2', 'interface Ethernet1-2', 'channel-group 1 mode active']) instance = dut.api('interfaces').get_instance('Port-Channel1') result = instance.get_members('Port-Channel1') self.assertEqual(result, ['Ethernet1', 'Ethernet2'], 'dut=%s' % dut) def test_set_lacp_mode(self): for dut in self.duts: for mode in ['on', 'active', 'passive']: cfgmode = 'on' if mode != 'on' else 'active' dut.config(['no interface Port-Channel1', 'default interface Ethernet1', 'interface Ethernet1', 'channel-group 1 mode %s' % cfgmode]) result = dut.api('interfaces').set_lacp_mode('Port-Channel1', mode) self.assertTrue(result, 'dut=%s' % dut) commands = 'show running-config interfaces Ethernet1' config = dut.run_commands(commands, 'text') self.assertIn('channel-group 1 mode %s' % mode, config[0]['output'], 'dut=%s' % dut) def test_set_lacp_mode_invalid_value(self): for dut in self.duts: mode = random_string() result = dut.api('interfaces').set_lacp_mode('Port-Channel1', mode) self.assertFalse(result) def test_get_lacp_mode_with_default(self): for dut in self.duts: dut.config(['no interface Port-Channel1', 'interface Port-Channel1']) instance = dut.api('interfaces').get_instance('Port-Channel1') result = instance.get_lacp_mode('Port-Channel1') self.assertEqual(result, 'on', 'dut=%s' % dut) def test_minimum_links_valid(self): for dut in self.duts: minlinks = random_int(1, 16) dut.config(['no interface Port-Channel1', 'interface Port-Channel1']) result = dut.api('interfaces').set_minimum_links('Port-Channel1', minlinks) self.assertTrue(result, 'dut=%s' % dut) commands = 'show running-config interfaces Port-Channel1' config = dut.run_commands(commands, 'text') self.assertIn('port-channel min-links %s' % minlinks, config[0]['output'], 'dut=%s' % dut) def test_minimum_links_invalid_value(self): for dut in self.duts: minlinks = random_int(129, 256) # some duts may support up to 128 result = dut.api( 'interfaces').set_minimum_links('Port-Channel1', minlinks) self.assertFalse(result) def test_create_and_delete_portchannel_sub_interface(self): for dut in self.duts: et1 = random_interface(dut) et2 = random_interface(dut, exclude=[et1]) dut.config(['no interface Port-Channel1', 'default interface %s' % et1, 'interface %s' % et1, 'channel-group 1 mode on', 'default interface %s' % et2, 'interface %s' % et2, 'channel-group 1 mode on']) # Create subint Port-Channel1.1 api = dut.api('interfaces') result = api.create('Port-Channel1.1') self.assertTrue(result, 'dut=%s' % dut) command = 'show running-config interfaces Port-Channel1.1' output = dut.run_commands(command, encoding='text') self.assertIn('Port-Channel1.1', output[0]['output']) # Delete subint Port-Channel1.1 result = dut.api('interfaces').delete('Port-Channel1.1') self.assertTrue(result) output = dut.run_commands(command, encoding='text') self.assertEqual(output[0]['output'], '') # Remove port-channel and default interfaces dut.config(['no interface Port-Channel1', 'default interface %s' % et1, 'default interface %s' % et2]) def test_set_and_unset_portchannel_sub_intf_encapsulation(self): for dut in self.duts: et1 = random_interface(dut) et2 = random_interface(dut, exclude=[et1]) dut.config(['no interface Port-Channel1', 'default interface %s' % et1, 'interface %s' % et1, 'channel-group 1 mode on', 'default interface %s' % et2, 'interface %s' % et2, 'channel-group 1 mode on']) # Create subint Port-Channel1.1 api = dut.api('interfaces') result = api.create('Port-Channel1.1') self.assertTrue(result) # Set encapsulation result = api.set_encapsulation('Port-Channel1.1', 4) self.assertTrue(result) command = 'show running-config interfaces Port-Channel1.1' output = dut.run_commands(command, encoding='text') encap = 'encapsulation dot1q vlan 4' self.assertIn(encap, output[0]['output']) # Unset encapsulation result = api.set_encapsulation('Port-Channel1.1', 4, default=True) self.assertTrue(result) output = dut.run_commands(command, encoding='text') self.assertNotIn(encap, output[0]['output']) # Delete subint Port-Channel1.1 result = dut.api('interfaces').delete('Port-Channel1.1') self.assertTrue(result) output = dut.run_commands(command, encoding='text') self.assertEqual(output[0]['output'], '') # Remove port-channel and default interfaces dut.config(['no interface Port-Channel1', 'default interface %s' % et1, 'default interface %s' % et2]) class TestApiVxlanInterface(DutSystemTest): def test_get(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) result = dut.api('interfaces').get('Vxlan1') self.assertIsInstance(result, dict) self.assertEqual(result['type'], 'vxlan') self.assertEqual(result['name'], 'Vxlan1') self.assertFalse(result['shutdown']) self.assertEqual(result['description'], None) self.assertEqual(result['source_interface'], '') self.assertEqual(result['multicast_group'], '') self.assertEqual(result['multicast_decap'], False) def get_config(self, dut): cmd = 'show running-config all interfaces Vxlan1' config = dut.run_commands(cmd, 'text') return config[0]['output'] def contains(self, text, dut): self.assertIn(text, self.get_config(dut), 'dut=%s' % dut) def notcontains(self, text, dut): self.assertNotIn(text, self.get_config(dut), 'dut=%s' % dut) def may_contain( self, text, dut, first=False, last=False ): """handles multiple variants of cli, typically to handle deprecated variants of the cli. The first and last calls in the variant sequence must be initialized respectively. At least one call in the sequence should result in a positive assertion """ if first: self.skip_rest = False if self.skip_rest: return if last: self.assertIn(text, self.get_config(dut), 'dut=%s' % dut) return try: self.assertIn(text, self.get_config(dut), 'dut=%s' % dut) self.skip_rest = True except AssertionError: pass def test_set_source_interface(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) api = dut.api('interfaces') instance = api.set_source_interface('Vxlan1', 'Loopback0') self.assertTrue(instance) self.contains('vxlan source-interface Loopback0', dut) def test_set_source_interface_default(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1', 'vxlan source-interface Loopback0']) api = dut.api('interfaces') instance = api.set_source_interface('Vxlan1', default=True) self.assertTrue(instance) self.contains('no vxlan source-interface', dut) def test_set_source_interface_negate(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1', 'vxlan source-interface Loopback0']) api = dut.api('interfaces') instance = api.set_source_interface('Vxlan1', disable=True) self.assertTrue(instance) self.contains('no vxlan source-interface', dut) def test_set_multicast_group(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) api = dut.api('interfaces') instance = api.set_multicast_group('Vxlan1', '239.10.10.10') self.assertTrue(instance) self.contains('vxlan multicast-group', dut) self.contains('239.10.10.10', dut) def test_set_multicast_group_default(self): for dut in self.duts: dut.config( ['no interface Vxlan1', 'interface Vxlan1', CliVariants( 'vxlan multicast-group decap 239.10.10.10', 'vxlan multicast-group 239.10.10.10') ]) api = dut.api('interfaces') instance = api.set_multicast_group('Vxlan1', default=True) self.assertTrue(instance) self.contains('no vxlan multicast-group', dut) def test_set_multicast_group_negate(self): for dut in self.duts: dut.config( ['no interface Vxlan1', 'interface Vxlan1', CliVariants( 'vxlan multicast-group decap 239.10.10.10', 'vxlan multicast-group 239.10.10.10') ]) api = dut.api('interfaces') instance = api.set_multicast_group('Vxlan1', disable=True) self.assertTrue(instance) self.contains('no vxlan multicast-group', dut) '''commenting this one out as it will only parse on a trident based DUT def test_set_multicast_decap(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) api = dut.api('interfaces') instance = api.set_multicast_decap('Vxlan1') self.assertTrue(instance) self.contains('vxlan multicast-group decap', dut) ''' def test_set_udp_port(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1', 'vxlan udp-port 1024']) api = dut.api('interfaces') instance = api.set_udp_port('Vxlan1', '1024') self.assertTrue(instance) self.contains('vxlan udp-port 1024', dut) def test_set_udp_port_default(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1', 'vxlan udp-port 1024']) api = dut.api('interfaces') instance = api.set_udp_port('Vxlan1', default=True) self.assertTrue(instance) self.contains('vxlan udp-port 4789', dut) def test_set_udp_port_negate(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1', 'vxlan udp-port 1024']) api = dut.api('interfaces') instance = api.set_udp_port('Vxlan1', disable=True) self.assertTrue(instance) self.contains('vxlan udp-port 4789', dut) def test_add_vtep(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) api = dut.api('interfaces') instance = api.add_vtep('Vxlan1', '1.1.1.1') self.assertTrue(instance) self.contains('vxlan flood vtep 1.1.1.1', dut) def test_add_vtep_to_vlan(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) api = dut.api('interfaces') instance = api.add_vtep('Vxlan1', '1.1.1.1', vlan='10') self.assertTrue(instance) self.contains('vxlan vlan 10 flood vtep 1.1.1.1', dut) def test_remove_vtep(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) api = dut.api('interfaces') instance = api.remove_vtep('Vxlan1', '1.1.1.1') self.assertTrue(instance) self.contains('no vxlan flood vtep', dut) def test_remove_vtep_from_vlan(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) api = dut.api('interfaces') instance = api.remove_vtep('Vxlan1', '1.1.1.1', vlan='10') self.assertTrue(instance) self.notcontains('vxlan vlan 10 flood vtep remove 1.1.1.1', dut) def test_update_vlan(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) api = dut.api('interfaces') instance = api.update_vlan('Vxlan1', '10', '10') self.assertTrue(instance) self.may_contain('vxlan vlan 10 vni 10', dut, first=True) self.may_contain('vxlan vlan add 10 vni 10', dut, last=True) def test_remove_vlan(self): for dut in self.duts: dut.config(['no interface Vxlan1', 'interface Vxlan1']) api = dut.api('interfaces') instance = api.remove_vlan('Vxlan1', '10') self.assertTrue(instance) self.notcontains('vxlan vlan remove 10 vni 10 $', dut) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_ipinterfaces.py0000644000076500000240000001372714447405743023406 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import time import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest, random_interface class TestResourceIpinterfaces(DutSystemTest): def test_get(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'no switchport', 'ip address 99.98.99.99/24', 'mtu 1800']) result = dut.api('ipinterfaces').get(intf) values = dict(name=intf, address='99.98.99.99/24', mtu=1800) self.assertEqual(values, result, 'dut=%s' % dut) def test_get_interface_wo_ip_adddress(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'no switchport']) time.sleep(2) result = dut.api('ipinterfaces').get(intf) self.assertIsNone(result['address']) def test_getall(self): for dut in self.duts: result = dut.api('interfaces').getall() self.assertIsInstance(result, dict) for intf in ['Management1']: self.assertIn(intf, result) def test_create_and_return_true(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) resource = dut.api('ipinterfaces') result = resource.create(intf) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertIn('no switchport', config[0]['output']) dut.config('default interface %s' % intf) def test_delete_and_return_true(self): for dut in self.duts: intf = random_interface(dut) dut.config(['interface %s' % intf, 'ip address 199.1.1.1/24']) resource = dut.api('ipinterfaces') result = resource.delete(intf) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertNotIn('ip address 199.1.1.1/24', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) def test_set_address(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'no switchport']) resource = dut.api('ipinterfaces') result = resource.set_address(intf, '111.111.111.111/24') self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertIn('ip address 111.111.111.111/24', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) def test_set_mtu(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'ip address 111.111.111.111/24']) resource = dut.api('ipinterfaces') result = resource.set_mtu(intf, 2000) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertIn('mtu 2000', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) def test_set_mtu_value_as_string(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'ip address 111.111.111.111/24']) resource = dut.api('ipinterfaces') result = resource.set_mtu(intf, '2000') self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertIn('mtu 2000', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_mlag.py0000644000076500000240000002642614447405743021652 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest class TestApiMlag(DutSystemTest): def test_get(self): for dut in self.duts: dut.config(['no interface Port-Channel1-2000', 'default mlag configuration']) response = dut.api('mlag').get() config = dict(domain_id=None, local_interface=None, peer_link=None, peer_address=None, shutdown=False) values = dict(config=config, interfaces=dict()) self.assertEqual(values, response) def test_set_domain_id_with_value(self): for dut in self.duts: dut.config('default mlag configuration') api = dut.api('mlag') self.assertIn('no domain-id', api.get_block('mlag configuration')) for domid in ['test_domain_id', 'test.dom-id', 'test domain id']: result = dut.api('mlag').set_domain_id(domid) self.assertTrue(result) self.assertIn('domain-id %s' % domid, api.get_block('mlag configuration')) def test_set_domain_id_with_no_value(self): for dut in self.duts: dut.config(['mlag configuration', 'domain-id test']) api = dut.api('mlag') self.assertIn('domain-id test', api.get_block('mlag configuration')) result = dut.api('mlag').set_domain_id(disable=True) self.assertTrue(result) self.assertIn('no domain-id', api.get_block('mlag configuration')) def test_set_domain_id_with_default(self): for dut in self.duts: dut.config(['mlag configuration', 'domain-id test']) api = dut.api('mlag') self.assertIn('domain-id test', api.get_block('mlag configuration')) result = dut.api('mlag').set_domain_id(default=True) self.assertTrue(result) self.assertIn('no domain-id', api.get_block('mlag configuration')) def test_set_local_interface_with_value(self): for dut in self.duts: dut.config('default mlag configuration') api = dut.api('mlag') self.assertIn('no local-interface', api.get_block('mlag configuration')) result = dut.api('mlag').set_local_interface('Vlan1234') self.assertTrue(result) self.assertIn('local-interface Vlan1234', api.get_block('mlag configuration')) def test_set_local_interface_with_no_value(self): for dut in self.duts: dut.config(['interface Vlan1234', 'mlag configuration', 'local-interface Vlan1234']) api = dut.api('mlag') self.assertIn('local-interface Vlan1234', api.get_block('mlag configuration')) result = api.set_local_interface(disable=True) self.assertTrue(result) self.assertIn('no local-interface', api.get_block('mlag configuration')) def test_set_local_interface_with_default(self): for dut in self.duts: dut.config(['interface Vlan1234', 'mlag configuration', 'local-interface Vlan1234']) api = dut.api('mlag') self.assertIn('local-interface Vlan1234', api.get_block('mlag configuration')) result = api.set_local_interface(default=True) self.assertTrue(result) self.assertIn('no local-interface', api.get_block('mlag configuration')) def test_set_peer_address_with_value(self): for dut in self.duts: dut.config('default mlag configuration') api = dut.api('mlag') self.assertIn('no peer-address', api.get_block('mlag configuration')) result = dut.api('mlag').set_peer_address('1.2.3.4') self.assertTrue(result) self.assertIn('peer-address 1.2.3.4', api.get_block('mlag configuration')) def test_set_peer_address_with_no_value(self): for dut in self.duts: dut.config(['interface Vlan1234', 'ip address 1.2.3.1/24', 'mlag configuration', 'peer-address 1.2.3.4']) api = dut.api('mlag') self.assertIn('peer-address 1.2.3.4', api.get_block('mlag configuration')) result = api.set_peer_address(disable=True) self.assertTrue(result) self.assertIn('no peer-address', api.get_block('mlag configuration')) def test_set_peer_address_with_default(self): for dut in self.duts: dut.config(['interface Vlan1234', 'ip address 1.2.3.1/24', 'mlag configuration', 'peer-address 1.2.3.4']) api = dut.api('mlag') self.assertIn('peer-address 1.2.3.4', api.get_block('mlag configuration')) result = api.set_peer_address(default=True) self.assertTrue(result) self.assertIn('no peer-address', api.get_block('mlag configuration')) def test_set_peer_link_with_value(self): for dut in self.duts: dut.config('default mlag configuration') api = dut.api('mlag') self.assertIn('no peer-link', api.get_block('mlag configuration')) result = dut.api('mlag').set_peer_link('Ethernet1') self.assertTrue(result) self.assertIn('peer-link Ethernet1', api.get_block('mlag configuration')) def test_set_peer_link_with_value_portchannel(self): for dut in self.duts: dut.config(['default mlag configuration', 'interface Port-Channel5']) api = dut.api('mlag') self.assertIn('no peer-link', api.get_block('mlag configuration')) result = dut.api('mlag').set_peer_link('Port-Channel5') self.assertTrue(result) self.assertIn('peer-link Port-Channel5', api.get_block('mlag configuration')) def test_set_peer_link_with_no_value(self): for dut in self.duts: dut.config(['mlag configuration', 'peer-link Ethernet1']) api = dut.api('mlag') self.assertIn('peer-link Ethernet1', api.get_block('mlag configuration')) result = api.set_peer_link(disable=True) self.assertTrue(result) self.assertIn('no peer-link', api.get_block('mlag configuration')) def test_set_peer_link_with_default(self): for dut in self.duts: dut.config(['mlag configuration', 'peer-link Ethernet1']) api = dut.api('mlag') self.assertIn('peer-link Ethernet1', api.get_block('mlag configuration')) result = api.set_peer_link(default=True) self.assertTrue(result) self.assertIn('no peer-link', api.get_block('mlag configuration')) def test_set_shutdown_with_true(self): for dut in self.duts: dut.config('default mlag configuration') api = dut.api('mlag') self.assertIn('no shutdown', api.get_block('mlag configuration')) result = api.set_shutdown(True) self.assertTrue(result) self.assertIn('shutdown', api.get_block('mlag configuration')) def test_set_shutdown_with_false(self): for dut in self.duts: dut.config(['mlag configuration', 'shutdown']) api = dut.api('mlag') self.assertIn('shutdown', api.get_block('mlag configuration')) result = api.set_shutdown(False) self.assertTrue(result) self.assertIn('no shutdown', api.get_block('mlag configuration')) def test_set_shutdown_with_no_value(self): for dut in self.duts: dut.config(['mlag configuration', 'shutdown']) api = dut.api('mlag') self.assertIn('shutdown', api.get_block('mlag configuration')) result = api.set_shutdown(disable=True) self.assertTrue(result) self.assertIn('no shutdown', api.get_block('mlag configuration')) def test_set_shutdown_with_default(self): for dut in self.duts: dut.config(['mlag configuration', 'shutdown']) api = dut.api('mlag') self.assertIn('shutdown', api.get_block('mlag configuration')) result = api.set_shutdown(default=True) self.assertTrue(result) self.assertIn('no shutdown', api.get_block('mlag configuration')) def test_set_mlag_id_with_value(self): for dut in self.duts: dut.config('no interface Port-Channel10') api = dut.api('mlag') self.assertIsNone(api.get_block('interface Port-Channel10')) result = api.set_mlag_id('Port-Channel10', '100') self.assertTrue(result) self.assertIn('mlag 100', api.get_block('interface Port-Channel10')) def test_set_mlag_id_with_no_value(self): for dut in self.duts: dut.config(['no interface Port-Channel10', 'interface Port-Channel10', 'mlag 100']) api = dut.api('mlag') self.assertIn('mlag 100', api.get_block('interface Port-Channel10')) result = api.set_mlag_id('Port-Channel10', disable=True) self.assertTrue(result) self.assertIn('no mlag', api.get_block('interface Port-Channel10')) def test_set_mlag_id_with_default(self): for dut in self.duts: dut.config(['no interface Port-Channel10', 'interface Port-Channel10', 'mlag 100']) api = dut.api('mlag') self.assertIn('mlag 100', api.get_block('interface Port-Channel10')) result = api.set_mlag_id('Port-Channel10', default=True) self.assertTrue(result) self.assertIn('no mlag', api.get_block('interface Port-Channel10')) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_ntp.py0000644000076500000240000001740014447405743021523 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import itertools import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest class TestApiNtp(DutSystemTest): def test_get(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Ethernet1', 'ntp server 99.99.1.1']) else: dut.config(['ntp source Ethernet1', 'ntp server 99.99.1.1']) response = dut.api('ntp').get() self.assertIsNotNone(response) def test_create(self): intf = 'Ethernet1' for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no ntp local-interface']) else: dut.config(['no ntp source']) response = dut.api('ntp').create(intf) self.assertTrue(response) response = dut.api('ntp').get() self.assertEqual(response['source_interface'], intf) def test_delete(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Ethernet1']) else: dut.config(['ntp source Ethernet1']) response = dut.api('ntp').delete() self.assertTrue(response) response = dut.api('ntp').get() self.assertIsNone(response['source_interface']) def test_default(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Ethernet1']) else: dut.config(['ntp source Ethernet1']) response = dut.api('ntp').default() self.assertTrue(response) response = dut.api('ntp').get() self.assertIsNone(response['source_interface']) def test_set_source_interface(self): intf = 'Ethernet1' for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Loopback0']) else: dut.config(['ntp source Loopback0']) response = dut.api('ntp').set_source_interface(intf) self.assertTrue(response) response = dut.api('ntp').get() self.assertEqual(response['source_interface'], intf) def test_add_server_single(self): server = '10.10.10.35' for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Ethernet1', 'no ntp']) else: dut.config(['ntp source Ethernet1', 'no ntp']) response = dut.api('ntp').add_server(server) self.assertTrue(response) response = dut.api('ntp').get() keys = [x.keys() for x in response['servers']] keys = list(itertools.chain.from_iterable(keys)) self.assertListEqual(keys, [server]) def test_add_server_multiple(self): servers = ['10.10.10.37', '10.10.10.36', '10.10.10.34'] for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Ethernet1', 'no ntp']) else: dut.config(['ntp source Ethernet1', 'no ntp']) for server in servers: response = dut.api('ntp').add_server(server) self.assertTrue(response) response = dut.api('ntp').get() keys = [x.keys() for x in response['servers']] keys = list(itertools.chain.from_iterable(keys)) self.assertListEqual(sorted(keys), sorted(servers)) def test_add_server_prefer(self): server = '10.10.10.35' for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Ethernet1', 'no ntp']) else: dut.config(['ntp source Ethernet1', 'no ntp']) response = dut.api('ntp').add_server(server, prefer=False) self.assertTrue(response) response = dut.api('ntp').get() self.assertIsNone(response['servers'][0][server]) response = dut.api('ntp').add_server(server, prefer=True) self.assertTrue(response) response = dut.api('ntp').get() self.assertEqual(response['servers'][0][server], 'prefer') def test_add_server_invalid(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Ethernet1', 'no ntp']) else: dut.config(['ntp source Ethernet1', 'no ntp']) with self.assertRaises(ValueError): dut.api('ntp').add_server(None) dut.api('ntp').add_server('') dut.api('ntp').add_server(' ') def test_remove_server(self): server = '10.10.10.35' servers = ['10.10.10.37', '10.10.10.36', '10.10.10.34'] for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Ethernet1', 'no ntp', 'ntp server %s' % server]) else: dut.config(['ntp source Ethernet1', 'no ntp', 'ntp server %s' % server]) for addserver in servers: dut.config(['ntp server %s' % addserver]) response = dut.api('ntp').remove_server(server) self.assertTrue(response) response = dut.api('ntp').get() keys = [x.keys() for x in response['servers']] keys = list(itertools.chain.from_iterable(keys)) self.assertListEqual(sorted(keys), sorted(servers)) def test_remove_all_servers(self): servers = ['10.10.10.37', '10.10.10.36', '10.10.10.34'] for dut in self.duts: if dut.version_number >= '4.23': dut.config(['ntp local-interface Ethernet1', 'no ntp']) else: dut.config(['ntp source Ethernet1', 'no ntp']) for addserver in servers: dut.config(['ntp server %s' % addserver]) response = dut.api('ntp').remove_all_servers() self.assertTrue(response) response = dut.api('ntp').get() self.assertEqual(response['servers'], []) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_ospf.py0000644000076500000240000002161514447405743021674 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2016, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from random import randint from systestlib import DutSystemTest def clear_ospf_config(dut, pid=None): if pid is None: try: pid = int(dut.get_config(params="section ospf")[0].split()[2]) dut.config(['no router ospf %d' % pid]) except IndexError: '''No OSPF configured''' pass else: dut.config(['no router ospf %d' % pid]) class TestApiOspf(DutSystemTest): def test_get(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 1", "router-id 1.1.1.1", "network 2.2.2.0/24 area 0", "redistribute bgp"]) ospf_response = dut.api('ospf').get() config = dict(router_id="1.1.1.1", ospf_process_id=1, vrf='default', networks=[dict(netmask='24', network="2.2.2.0", area="0.0.0.0")], redistributions=[dict(protocol="bgp")], shutdown=False) self.assertEqual(ospf_response, config) def test_get_with_vrf(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 10 vrf test", "router-id 1.1.1.2", "network 2.2.2.0/24 area 0", "redistribute bgp"]) ospf_response = dut.api('ospf').get() config = dict(router_id="1.1.1.2", ospf_process_id=10, vrf='test', networks=[dict(netmask='24', network="2.2.2.0", area="0.0.0.0")], redistributions=[dict(protocol="bgp")], shutdown=False) self.assertEqual(ospf_response, config) clear_ospf_config(dut, 10) def test_shutdown(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 1", "network 1.1.1.1/32 area 0"]) ospf = dut.api('ospf') response = ospf.set_shutdown() self.assertTrue(response) self.assertIn('shutdown', ospf.get_block("router ospf 1")) def test_no_shutown(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 10", "network 1.1.1.0/24 area 0", "shutdown"]) ospf = dut.api('ospf') response = ospf.set_no_shutdown() self.assertTrue(response) self.assertIn('no shutdown', ospf.get_block("router ospf 10")) def test_delete(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 10"]) ospf = dut.api("ospf") response = ospf.delete() self.assertTrue(response) self.assertEqual(None, ospf.get_block("router ospf")) def test_create_valid_id(self): for dut in self.duts: clear_ospf_config(dut) pid = randint(1, 65536) ospf = dut.api("ospf") response = ospf.create(pid) self.assertTrue(response) self.assertIn("router ospf {}".format(pid), dut.get_config()) def test_create_invalid_id(self): for dut in self.duts: clear_ospf_config(dut) pid = randint(70000, 100000) with self.assertRaises(ValueError): dut.api("ospf").create(pid) def test_create_with_vrf(self): for dut in self.duts: clear_ospf_config(dut) pid = randint(1, 65536) ospf = dut.api("ospf") response = ospf.create(pid, vrf='test') self.assertTrue(response) self.assertIn("router ospf {} vrf {}".format(pid, 'test'), dut.get_config()) def test_configure_ospf(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 1"]) ospf = dut.api("ospf") response = ospf.configure_ospf("router-id 1.1.1.1") self.assertTrue(response) self.assertIn("router-id 1.1.1.1", ospf.get_block("router ospf 1")) def test_set_router_id(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 1"]) ospf = dut.api("ospf") response = ospf.set_router_id(randint(1, 65536)) self.assertFalse(response) response = ospf.set_router_id("2.2.2.2") self.assertTrue(response) self.assertIn("router-id 2.2.2.2", ospf.get_block("router ospf 1")) response = ospf.set_router_id(default=True) self.assertTrue(response) self.assertIn("no router-id", ospf.get_block("router ospf 1")) response = ospf.set_router_id(disable=True) self.assertTrue(response) self.assertIn("no router-id", ospf.get_block("router ospf 1")) def test_add_network(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 1"]) ospf = dut.api("ospf") response = ospf.add_network("2.2.2.0", "24", 1234) self.assertTrue(response) self.assertIn("network 2.2.2.0/24 area 0.0.4.210", ospf.get_block("router ospf 1")) response = ospf.add_network("10.10.10.0", "24") self.assertTrue(response) self.assertIn("network 10.10.10.0/24 area 0.0.0.0", ospf.get_block("router ospf 1")) def test_remove_network(self): for dut in self.duts: clear_ospf_config(dut) ospf_config = ["router ospf 1", "network 2.2.2.0/24 area 0.0.0.0", "network 3.3.3.1/32 area 1.1.1.1"] dut.config(ospf_config) ospf = dut.api("ospf") response = ospf.remove_network("2.2.2.0", "24") self.assertTrue(response) response = ospf.remove_network("3.3.3.1", "32", "1.1.1.1") self.assertTrue(response) for config in ospf_config: if "router ospf" not in config: self.assertNotIn(config, ospf.get_block("router ospf 1")) def test_add_redistribution(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 1"]) ospf = dut.api("ospf") protos = ['bgp', 'rip', 'static', 'connected'] for proto in protos: if randint(1, 10) % 2 == 0: response = ospf.add_redistribution(proto, 'test') else: response = ospf.add_redistribution(proto) self.assertTrue(response) for proto in protos: self.assertIn("redistribute {}".format(proto), ospf.get_block("router ospf 1")) with self.assertRaises(ValueError): ospf.add_redistribution("NOT VALID") def test_remove_redistribution(self): for dut in self.duts: clear_ospf_config(dut) dut.config(["router ospf 1", "redistribute bgp", "redistribute static route-map test"]) ospf = dut.api("ospf") response = ospf.remove_redistribution('bgp') self.assertTrue(response) response = ospf.remove_redistribution('static') self.assertTrue(response) self.assertNotIn("redistribute", ospf.get_block("router ospf 1")) pyeapi-1.0.2/test/system/test_api_routemaps.py0000644000076500000240000003010514447405743022736 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest from testlib import random_string class TestApiRoutemaps(DutSystemTest): def test_get(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'set weight 100', 'match tag 50']) response = dut.api('routemaps').get('TEST') self.assertIsNotNone(response) def test_get_none(self): for dut in self.duts: dut.config('no route-map TEST deny 10') result = dut.api('routemaps').get('TEST') self.assertIsNone(result) def test_getall(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'set weight 100', 'no route-map TEST-2 permit 50', 'route-map TEST-2 permit 50', 'match tag 50']) result = dut.api('routemaps').getall() self.assertIn(('TEST'), result) self.assertIn(('TEST-2'), result) def test_create(self): for dut in self.duts: dut.config(['no route-map TEST deny 10']) api = dut.api('routemaps') self.assertIsNone(api.get('TEST')) result = dut.api('routemaps').create('TEST', 'deny', 10) self.assertTrue(result) self.assertIsNotNone(api.get('TEST')) dut.config(['no route-map TEST deny 10']) def test_create_with_hyphen(self): for dut in self.duts: dut.config(['no route-map TEST-1 deny 10']) api = dut.api('routemaps') self.assertIsNone(api.get('TEST-1')) result = dut.api('routemaps').create('TEST-1', 'deny', 10) self.assertTrue(result) self.assertIsNotNone(api.get('TEST-1')) dut.config(['no route-map TEST-1 deny 10']) def test_create_with_underscore(self): for dut in self.duts: dut.config(['no route-map TEST_1 deny 10']) api = dut.api('routemaps') self.assertIsNone(api.get('TEST_1')) result = dut.api('routemaps').create('TEST_1', 'deny', 10) self.assertTrue(result) self.assertIsNotNone(api.get('TEST_1')) dut.config(['no route-map TEST_1 deny 10']) def test_delete(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'set weight 100']) api = dut.api('routemaps') self.assertIsNotNone(api.get('TEST')) result = dut.api('routemaps').delete('TEST', 'deny', 10) self.assertTrue(result) self.assertIsNone(api.get('TEST')) def test_default(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'set weight 100']) api = dut.api('routemaps') self.assertIsNotNone(api.get('TEST')) result = dut.api('routemaps').default('TEST', 'deny', 10) self.assertTrue(result) self.assertIsNone(api.get('TEST')) def test_set_description(self): for dut in self.duts: text = random_string() dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10']) api = dut.api('routemaps') self.assertNotIn('description %s' % text, api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_description('TEST', 'deny', 10, text) self.assertTrue(result) self.assertIn('description %s' % text, api.get_block('route-map TEST deny 10')) def test_set_match_statements(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10']) api = dut.api('routemaps') self.assertNotIn('match as 100', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_match_statements('TEST', 'deny', 10, ['as 100']) self.assertTrue(result) self.assertIn('match as 100', api.get_block('route-map TEST deny 10')) def test_update_match_statement(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'match as 100']) api = dut.api('routemaps') self.assertIn('match as 100', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_match_statements('TEST', 'deny', 10, ['as 200']) self.assertTrue(result) self.assertNotIn('match as 100', api.get_block('route-map TEST deny 10')) self.assertIn('match as 200', api.get_block('route-map TEST deny 10')) def test_remove_match_statement(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'match as 100']) api = dut.api('routemaps') self.assertIn('match as 100', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_match_statements('TEST', 'deny', 10, ['tag 50']) self.assertTrue(result) self.assertNotIn('match as 100', api.get_block('route-map TEST deny 10')) self.assertIn('match tag 50', api.get_block('route-map TEST deny 10')) def test_set_set_statements(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10']) api = dut.api('routemaps') self.assertNotIn('set weight 100', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_set_statements('TEST', 'deny', 10, ['weight 100']) self.assertTrue(result) self.assertIn('set weight 100', api.get_block('route-map TEST deny 10')) def test_update_set_statement(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'set weight 100']) api = dut.api('routemaps') self.assertIn('set weight 100', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_set_statements('TEST', 'deny', 10, ['weight 200']) self.assertTrue(result) self.assertNotIn('set weight 100', api.get_block('route-map TEST deny 10')) self.assertIn('set weight 200', api.get_block('route-map TEST deny 10')) def test_remove_set_statement(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'set weight 100']) api = dut.api('routemaps') self.assertIn('set weight 100', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_set_statements('TEST', 'deny', 10, ['tag 50']) self.assertTrue(result) self.assertNotIn('set weight 100', api.get_block('route-map TEST deny 10')) self.assertIn('set tag 50', api.get_block('route-map TEST deny 10')) def test_set_continue(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10']) api = dut.api('routemaps') self.assertNotIn('continue', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_continue('TEST', 'deny', 10, 100) self.assertTrue(result) self.assertEqual(100, api.get('TEST')['deny'][10]['continue']) def test_update_continue(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'continue 30']) api = dut.api('routemaps') self.assertIn('continue 30', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_continue('TEST', 'deny', 10, 60) self.assertTrue(result) self.assertEqual(60, api.get('TEST')['deny'][10]['continue']) def test_default_continue(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'continue 100']) api = dut.api('routemaps') self.assertIn('continue 100', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_continue('TEST', 'deny', 10, default=True) self.assertTrue(result) self.assertEqual(None, api.get('TEST')['deny'][10]['continue']) def test_negate_continue(self): for dut in self.duts: dut.config(['no route-map TEST deny 10', 'route-map TEST deny 10', 'continue 100']) api = dut.api('routemaps') self.assertIn('continue 100', api.get_block('route-map TEST deny 10')) result = dut.api('routemaps').set_continue('TEST', 'deny', 10, disable=True) self.assertTrue(result) self.assertEqual(None, api.get('TEST')['deny'][10]['continue']) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_staticroute.py0000644000076500000240000002403414447405743023271 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from random import choice from testlib import random_int, random_string from systestlib import DutSystemTest NEXT_HOPS = ['Ethernet1', 'Ethernet2', 'Null0', 'IP'] DISTANCES = TAGS = ROUTE_NAMES = [None, True] def _ip_addr(): ip1 = random_int(0, 223) ip2 = random_int(0, 255) ip3 = random_int(0, 255) return "%s.%s.%s.0/24" % (ip1, ip2, ip3) def _next_hop(): next_hop = choice(NEXT_HOPS) if next_hop == 'Null0': return (next_hop, None) ip1 = random_int(0, 223) ip2 = random_int(0, 255) ip3 = random_int(0, 255) ip4 = random_int(0, 255) ip_addr = "%s.%s.%s.%s" % (ip1, ip2, ip3, ip4) if next_hop == 'IP': return (ip_addr, None) return (next_hop, ip_addr) def _distance(): return random_int(1, 255) def _tag(): return random_int(0, 255) def _route_name(): return random_string(minchar=4, maxchar=10) class TestApiStaticroute(DutSystemTest): def test_create(self): # Validate the create function returns without an error # when creating routes with varying parameters included. for dut in self.duts: dut.config(['no ip routing', 'ip routing']) for t_distance in DISTANCES: for t_tag in TAGS: for t_route_name in ROUTE_NAMES: ip_dest = _ip_addr() (next_hop, next_hop_ip) = _next_hop() distance = t_distance if distance is True: distance = _distance() tag = t_tag if tag is True: tag = _tag() route_name = t_route_name if route_name is True: route_name = _route_name() result = dut.api('staticroute').create( ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, tag=tag, route_name=route_name) self.assertTrue(result) def test_get(self): # Validate the get function returns the exact value that # is passed in when the route exists on the switch. for dut in self.duts: dut.config(['no ip routing', 'ip routing']) ip_dest = '1.2.3.0/24' next_hop = 'Ethernet1' next_hop_ip = '1.1.1.1' distance = 1 tag = 1 route_name = 'test1' cmd = "ip route %s %s %s %s tag %s name %s" % \ (ip_dest, next_hop, next_hop_ip, distance, tag, route_name) dut.config([cmd]) route = { next_hop: { next_hop_ip: { distance: { 'tag': tag, 'route_name': route_name } } } } result = dut.api('staticroute').get(ip_dest) # Make sure the funtion returns a true result (match found) self.assertTrue(result) # Then make sure the returned string is what was expected # self.assertEqual(result.group(0), cmd) self.assertEqual(result, route) def test_getall(self): # Validate the get_all function returns a list of entries # containing the matched parameters, and that parameters # are matched in full (i.e. name 'test1' does not match # name 'test10'). for dut in self.duts: dut.config(['no ip routing', 'ip routing']) # Declare a set of 3 routes with same ip dest and next hop. # Set different distance, tag and name for each route, # including values 1 and 10 in each, so the test will verify # that matching 1 does not also match 10. route1 = \ 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1' route2 = \ 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test10' route3 = \ 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 2 tag 10 name test1' dut.config([route1, route2, route3]) routes = { '1.2.3.0/24': { 'Ethernet1': { '1.1.1.1': { 10: { 'tag': 1, 'route_name': 'test1' }, 1: { 'tag': 1, 'route_name': 'test10' }, 2: { 'tag': 10, 'route_name': 'test1' } } } } } # Get the list of ip routes from the switch result = dut.api('staticroute').getall() # Assert that the result dict is equivalent to the routes dict self.assertEqual(result['1.2.3.0/24'], routes['1.2.3.0/24']) def test_delete(self): # Validate the delete function returns without an error # when deleting routes with varying parameters included. # Note: the routes do not have to exist for the # delete command to succeed, but only that the command # does not error. for dut in self.duts: dut.config(['no ip routing', 'ip routing']) for t_distance in DISTANCES: for t_tag in TAGS: for t_route_name in ROUTE_NAMES: ip_dest = _ip_addr() (next_hop, next_hop_ip) = _next_hop() distance = t_distance if distance is True: distance = _distance() tag = t_tag if tag is True: tag = _tag() route_name = t_route_name if route_name is True: route_name = _route_name() result = dut.api('staticroute').delete( ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, tag=tag, route_name=route_name) self.assertTrue(result) def test_default(self): # Validate the default function returns without an error # when deleting routes with varying parameters included. # Note: currently EOS functionality of 'default ip route ...' # is equivalent to 'no ip route ...', which is the delete # function. for dut in self.duts: dut.config(['no ip routing', 'ip routing']) for t_distance in DISTANCES: for t_tag in TAGS: for t_route_name in ROUTE_NAMES: ip_dest = _ip_addr() (next_hop, next_hop_ip) = _next_hop() distance = t_distance if distance is True: distance = _distance() tag = t_tag if tag is True: tag = _tag() route_name = t_route_name if route_name is True: route_name = _route_name() result = dut.api('staticroute').default( ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, tag=tag, route_name=route_name) self.assertTrue(result) def test_set_tag(self): # Validate the set_tag function returns without an error # when modifying the tag on an existing route for dut in self.duts: dut.config(['no ip routing', 'ip routing', 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 99']) result = dut.api('staticroute').set_tag( '1.2.3.0/24', 'Ethernet1', next_hop_ip='1.1.1.1', distance=10, tag=3) self.assertTrue(result) def test_set_route_name(self): # Validate the set_route_name function returns without an error # when modifying the tag on an existing route for dut in self.duts: dut.config(['no ip routing', 'ip routing', 'ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 name test99']) result = dut.api('staticroute').set_route_name( '1.2.3.0/24', 'Ethernet1', next_hop_ip='1.1.1.1', distance=1, route_name='test3') self.assertTrue(result) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_stp.py0000644000076500000240000001405114447405743021527 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest, random_interface class TestApiStpInterfaces(DutSystemTest): def test_get(self): for dut in self.duts: dut.config(['default interface Ethernet1']) keys = ['portfast', 'portfast_type', 'bpduguard'] result = dut.api('stp').interfaces.get('Ethernet1') self.assertEqual(sorted(keys), sorted(result.keys()), 'dut=%s' % dut) def test_getall(self): for dut in self.duts: dut.config('default interface Et1-4') result = dut.api('stp').interfaces.getall() self.assertIsInstance(result, dict) def test_set_bpduguard_to_true(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) resource = dut.api('stp').interfaces result = resource.set_bpduguard(intf, True) self.assertTrue(result, 'dut=%s' % dut) def test_set_bpdugard_to_false(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'spanning-tree bpduguard enable']) resource = dut.api('stp').interfaces result = resource.set_bpduguard(intf, False) self.assertTrue(result, 'dut=%s' % dut) def test_set_bpdugard_to_default(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'spanning-tree bpduguard enable']) resource = dut.api('stp').interfaces result = resource.set_bpduguard(intf, default=True) self.assertTrue(result, 'dut=%s' % dut) def test_set_bpdugard_to_no(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'spanning-tree bpduguard enable']) resource = dut.api('stp').interfaces result = resource.set_bpduguard(intf, disable=True) self.assertTrue(result, 'dut=%s' % dut) def test_set_portfast_to_true(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) resource = dut.api('stp').interfaces result = resource.set_portfast(intf, True) self.assertTrue(result, 'dut=%s' % dut) def test_set_portfast_to_false(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'spanning-tree portfast']) resource = dut.api('stp').interfaces result = resource.set_portfast(intf, False) self.assertTrue(result, 'dut=%s' % dut) def test_set_portfast_to_default(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'spanning-tree portfast']) resource = dut.api('stp').interfaces result = resource.set_portfast(intf, default=True) self.assertTrue(result, 'dut=%s' % dut) def test_set_portfast_to_no(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'spanning-tree portfast']) resource = dut.api('stp').interfaces result = resource.set_portfast(intf, disable=True) self.assertTrue(result, 'dut=%s' % dut) def test_set_portfast_to_edge(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) resource = dut.api('stp').interfaces result = resource.set_portfast_type(intf, 'edge') self.assertTrue(result, 'dut=%s' % dut) def test_set_portfast_to_network(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'spanning-tree portfast']) resource = dut.api('stp').interfaces result = resource.set_portfast_type(intf, 'normal') self.assertTrue(result, 'dut=%s' % dut) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_switchports.py0000644000076500000240000001406714447405743023321 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest, random_interface class TestApiSwitchports(DutSystemTest): def test_get(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) result = dut.api('switchports').get(intf) self.assertIsInstance(result, dict) self.assertEqual(result['mode'], 'access') self.assertEqual(result['access_vlan'], '1') self.assertEqual(result['trunk_native_vlan'], '1') self.assertEqual(result['trunk_allowed_vlans'], '1-4094') def test_get_returns_none(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'no switchport']) result = dut.api('switchports').get(intf) self.assertIsNone(result) def test_getall(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) result = dut.api('switchports').getall() self.assertIsInstance(result, dict, 'dut=%s' % dut) self.assertIn(intf, result, 'dut=%s' % dut) def test_create_and_return_true(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'no switchport']) result = dut.api('switchports').create(intf) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertNotIn('no switchport', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) def test_delete_and_return_true(self): for dut in self.duts: intf = random_interface(dut) dut.config('default interface %s' % intf) result = dut.api('switchports').delete(intf) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertIn('no switchport', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) def test_set_access_vlan_to_value(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'vlan 100']) resource = dut.api('switchports') result = resource.set_access_vlan(intf, '100') self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertIn('switchport access vlan 100', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) def test_set_trunk_native_vlan(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'switchport mode trunk', 'vlan 100']) resource = dut.api('switchports') result = resource.set_trunk_native_vlan(intf, '100') self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertIn('switchport trunk native vlan 100', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) def test_set_trunk_allowed_vlans(self): for dut in self.duts: intf = random_interface(dut) dut.config(['default interface %s' % intf, 'interface %s' % intf, 'switchport mode trunk']) resource = dut.api('switchports') result = resource.set_trunk_allowed_vlans(intf, '1,10,100') self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show running-config interfaces %s' % intf, 'text') self.assertIn('switchport trunk allowed vlan 1,10,100', config[0]['output'], 'dut=%s' % dut) dut.config('default interface %s' % intf) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_system.py0000644000076500000240000002257414447405743022256 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import random_string from systestlib import DutSystemTest class TestApiSystem(DutSystemTest): def test_get(self): for dut in self.duts: dut.config('default hostname') resp = dut.api('system').get() keys = ['hostname', 'iprouting', 'banner_motd', 'banner_login'] self.assertEqual(sorted(keys), sorted(resp.keys())) def test_get_with_period(self): for dut in self.duts: dut.config('hostname host.domain.net') response = dut.api('system').get() self.assertEqual(response['hostname'], 'host.domain.net') def test_get_check_hostname(self): for dut in self.duts: dut.config('hostname teststring') response = dut.api('system').get() self.assertEqual(response['hostname'], 'teststring') def test_get_check_banners(self): for dut in self.duts: motd_banner_value = random_string() + "\n" login_banner_value = random_string() + "\n" dut.config([dict(cmd="banner motd", input=motd_banner_value)]) dut.config([dict(cmd="banner login", input=login_banner_value)]) resp = dut.api('system').get() self.assertEqual(resp['banner_login'], login_banner_value.rstrip()) self.assertEqual(resp['banner_motd'], motd_banner_value.rstrip()) def test_get_banner_with_EOF(self): for dut in self.duts: motd_banner_value = '!!!newlinebaner\nSecondLIneEOF!!!newlinebanner\n' dut.config([dict(cmd="banner motd", input=motd_banner_value)]) resp = dut.api('system').get() self.assertEqual(resp['banner_motd'], motd_banner_value.rstrip()) def test_set_hostname_with_value(self): for dut in self.duts: dut.config('default hostname') value = random_string() response = dut.api('system').set_hostname(value) self.assertTrue(response, 'dut=%s' % dut) value = 'hostname %s' % value self.assertIn(value, dut.running_config) def test_set_hostname_with_no_value(self): for dut in self.duts: dut.config('hostname test') response = dut.api('system').set_hostname(disable=True) self.assertTrue(response, 'dut=%s' % dut) value = 'no hostname' self.assertIn(value, dut.running_config) def test_set_hostname_with_default(self): for dut in self.duts: dut.config('hostname test') response = dut.api('system').set_hostname(default=True) self.assertTrue(response, 'dut=%s' % dut) value = 'no hostname' self.assertIn(value, dut.running_config) def test_set_hostname_default_over_value(self): for dut in self.duts: dut.config('hostname test') response = dut.api('system').set_hostname(value='foo', default=True) self.assertTrue(response, 'dut=%s' % dut) value = 'no hostname' self.assertIn(value, dut.running_config) def test_set_iprouting_to_true(self): for dut in self.duts: dut.config('no ip routing') resp = dut.api('system').set_iprouting(True) self.assertTrue(resp, 'dut=%s' % dut) self.assertNotIn('no ip rotuing', dut.running_config) def test_set_iprouting_to_false(self): for dut in self.duts: dut.config('ip routing') resp = dut.api('system').set_iprouting(False) self.assertTrue(resp, 'dut=%s' % dut) self.assertIn('no ip routing', dut.running_config) def test_set_iprouting_to_no(self): for dut in self.duts: dut.config('ip routing') resp = dut.api('system').set_iprouting(disable=True) self.assertTrue(resp, 'dut=%s' % dut) self.assertIn('no ip routing', dut.running_config) def test_set_iprouting_to_default(self): for dut in self.duts: dut.config('ip routing') resp = dut.api('system').set_iprouting(default=True) self.assertTrue(resp, 'dut=%s' % dut) self.assertIn('no ip routing', dut.running_config) def test_set_hostname_with_period(self): for dut in self.duts: dut.config('hostname localhost') response = dut.api('system').set_hostname(value='host.domain.net') self.assertTrue(response, 'dut=%s' % dut) value = 'hostname host.domain.net' self.assertIn(value, dut.running_config) def test_set_banner_motd(self): for dut in self.duts: banner_value = random_string() dut.config([dict(cmd="banner motd", input=banner_value)]) self.assertIn(banner_value, dut.running_config) banner_api_value = random_string() resp = dut.api('system').set_banner("motd", banner_api_value) self.assertTrue(resp, 'dut=%s' % dut) self.assertIn(banner_api_value, dut.running_config) def test_set_banner_motd_donkey(self): for dut in self.duts: donkey_chicken = r""" /\ /\ ( \\ // ) \ \\ // / \_\\||||//_/ \/ _ _ \ \/|(o)(O)| \/ | | ___________________\/ \ / // // |____| Cluck cluck cluck! // || / \ //| \| \ 0 0 / // \ ) V / \____/ // \ / ( / "" \ /_________| |_/ / /\ / | || / / / / \ || | | | | | || | | | | | || |_| |_| |_|| \_\ \_\ \_\\ """ resp = dut.api('system').set_banner("motd", donkey_chicken) self.assertTrue(resp, 'dut=%s' % dut) self.assertIn(donkey_chicken, dut.running_config) def test_set_banner_motd_default(self): for dut in self.duts: dut.config([dict(cmd="banner motd", input="!!!!REMOVE BANNER TEST!!!!")]) dut.api('system').set_banner('motd', None, True) self.assertIn('no banner motd', dut.running_config) def test_set_banner_login(self): for dut in self.duts: banner_value = random_string() dut.config([dict(cmd="banner login", input=banner_value)]) self.assertIn(banner_value, dut.running_config) banner_api_value = random_string() resp = dut.api('system').set_banner("login", banner_api_value) self.assertTrue(resp, 'dut=%s' % dut) self.assertIn(banner_api_value, dut.running_config) config_login_banner = dut.api('system').get()['banner_login'] self.assertTrue(config_login_banner, banner_api_value.strip()) def test_set_banner_login_default(self): for dut in self.duts: dut.config([dict(cmd="banner login", input="!!!!REMOVE LOGIN BANNER TEST!!!!")]) dut.api('system').set_banner('login', None, True) self.assertIn('no banner login', dut.running_config) def test_set_banner_login_negate(self): for dut in self.duts: dut.config([dict(cmd="banner login", input="!!!!REMOVE LOGIN BANNER TEST!!!!")]) dut.api('system').set_banner('login', None, False, True) self.assertIn('no banner login', dut.running_config) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_users.py0000644000076500000240000002271614447405743022071 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest TEST_SSH_KEY = ('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKL1UtBALa4CvFUsHUipNym' 'A04qCXuAtTwNcMj84bTUzUI+q7mdzRCTLkllXeVxKuBnaTm2PW7W67K5CVpl0' 'EVCm6IY7FS7kc4nlnD/tFvTvShy/fzYQRAdM7ZfVtegW8sMSFJzBR/T/Y/sxI' '16Y/dQb8fC3la9T25XOrzsFrQiKRZmJGwg8d+0RLxpfMg0s/9ATwQKp6tPoLE' '4f3dKlAgSk5eENyVLA3RsypWADHpenHPcB7sa8D38e1TS+n+EUyAdb3Yov+5E' 'SAbgLIJLd52Xv+FyYi0c2L49ByBjcRrupp4zfXn4DNRnEG4K6GcmswHuMEGZv' '5vjJ9OYaaaaaaa') class TestApiUsers(DutSystemTest): def test_get(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no username test', 'username test nopassword', 'username test ssh-key %s' % TEST_SSH_KEY]) else: dut.config(['no username test', 'username test nopassword', 'username test sshkey %s' % TEST_SSH_KEY]) result = dut.api('users').get('test') if dut.version_number >= '4.23': values = dict(nopassword=True, privilege='1', secret='', role='', format='') values["ssh-key"] = TEST_SSH_KEY else: values = dict(nopassword=True, privilege='1', secret='', role='', format='', sshkey=TEST_SSH_KEY) result = self.sort_dict_by_keys(result) values = self.sort_dict_by_keys(values) self.assertEqual(values, result) def test_getall(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) result = dut.api('users').getall() self.assertIsInstance(result, dict, 'dut=%s' % dut) self.assertIn('test', result, 'dut=%s' % dut) def test_create(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) api = dut.api('users') result = api.create('test', nopassword=True) self.assertTrue(result) self.assertIn('username test privilege 1', api.config) def test_delete(self): for dut in self.duts: dut.config(['username test nopassword']) api = dut.api('users') self.assertIn('username test privilege 1 nopassword', api.config) result = api.delete('test') self.assertTrue(result) self.assertNotIn('username test privilege 1 nopassword', api.config) def test_default(self): for dut in self.duts: dut.config(['username test nopassword']) api = dut.api('users') self.assertIn('username test privilege 1 nopassword', api.config) result = api.default('test') self.assertTrue(result) self.assertNotIn('username test nopassword', api.config) def test_set_privilege_with_value(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) api = dut.api('users') # EOS defaults to privilege 1 self.assertIn('username test privilege 1 nopassword', api.config) result = api.set_privilege('test', 8) self.assertTrue(result) self.assertIn('username test privilege 8 nopassword', api.config) def test_set_privilege_with_no_value(self): for dut in self.duts: dut.config(['no username test', 'username test privilege 8 nopassword']) api = dut.api('users') self.assertIn('username test privilege 8', api.config) result = api.set_privilege('test') self.assertTrue(result) self.assertIn('username test privilege 1', api.config) def test_set_role_with_value(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) api = dut.api('users') self.assertIn('username test privilege 1 nopassword', api.config) result = api.set_role('test', 'network-admin') self.assertTrue(result) self.assertIn('username test privilege 1 role network-admin nopassword', api.config) def test_set_role_with_no_value(self): for dut in self.duts: dut.config(['no username test', 'username test role network-admin nopassword']) api = dut.api('users') self.assertIn('username test privilege 1 role network-admin nopassword', api.config) result = api.set_role('test') self.assertTrue(result) self.assertNotIn('username test privilege 1 role network-admin nopassword', api.config) def test_set_sshkey_with_value(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) api = dut.api('users') self.assertIn('username test privilege 1 nopassword', api.config) if dut.version_number >= '4.23': self.assertNotIn('username test ssh-key', api.config) else: self.assertNotIn('username test sshkey', api.config) result = api.set_sshkey('test', TEST_SSH_KEY) self.assertTrue(result) if dut.version_number >= '4.23': self.assertIn('username test ssh-key %s' % TEST_SSH_KEY, api.config) else: self.assertIn('username test sshkey %s' % TEST_SSH_KEY, api.config) def test_set_sshkey_with_empty_string(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) api = dut.api('users') self.assertIn('username test privilege 1 nopassword', api.config) if dut.version_number >= '4.23': self.assertNotIn('username test ssh-key', api.config) else: self.assertNotIn('username test sshkey', api.config) result = api.set_sshkey('test', '') self.assertTrue(result) if dut.version_number >= '4.23': self.assertNotIn('username test ssh-key %s' % TEST_SSH_KEY, api.config) else: self.assertNotIn('username test sshkey %s' % TEST_SSH_KEY, api.config) def test_set_sshkey_with_None(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) api = dut.api('users') self.assertIn('username test privilege 1 nopassword', api.config) if dut.version_number >= '4.23': self.assertNotIn('username test ssh-key', api.config) else: self.assertNotIn('username test sshkey', api.config) result = api.set_sshkey('test', None) self.assertTrue(result) if dut.version_number >= '4.23': self.assertNotIn('username test ssh-key %s' % TEST_SSH_KEY, api.config) else: self.assertNotIn('username test sshkey %s' % TEST_SSH_KEY, api.config) def test_set_sshkey_with_no_value(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) api = dut.api('users') self.assertIn('username test privilege 1 nopassword', api.config) result = api.set_sshkey('test', disable=True) self.assertTrue(result) if dut.version_number >= '4.23': self.assertNotIn('username test ssh-key %s' % TEST_SSH_KEY, api.config) else: self.assertNotIn('username test sshkey %s' % TEST_SSH_KEY, api.config) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_varp.py0000644000076500000240000003032414447405743021672 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest VIRT_ENTRY_A = 'ip virtual-router mac-address 00:11:22:33:44:55' VIRT_ENTRY_B = 'ip virtual-router mac-address 00:11:22:33:44:56' VIRT_ENTRY_C = 'ip virtual-router mac-address 00:11:22:33:44:57' IP_CMD = 'ip virtual-router address' class TestApiVarp(DutSystemTest): def _null_virtual_mac_command(self, dut): # Default format of ip virtual-router mac-address changed in EOS 4.17.x # pre 4.17.x - no ip virtual-router mac-address # 4.17.x and on - ip virtual-router mac-address 00:00:00:00:00:00 mac_dict = dut.api('varp')._parse_mac_address() virt_null_base = 'no ip virtual-router mac-address' if mac_dict['mac_address']: return virt_null_base + ' ' + mac_dict['mac_address'] return virt_null_base def test_basic_get(self): for dut in self.duts: virt_null = self._null_virtual_mac_command(dut) dut.config([virt_null]) response = dut.api('varp').get() self.assertIsNotNone(response) def test_get_with_value(self): for dut in self.duts: virt_null = self._null_virtual_mac_command(dut) dut.config([virt_null, VIRT_ENTRY_A]) response = dut.api('varp').get() self.assertIsNotNone(response) self.assertEqual(response['mac_address'], '00:11:22:33:44:55') def test_get_none(self): # None response is for code versions before 4.17.x due to default # configuration change in 4.17.x. Default configuration difference # shown above in _null_virtual_mac_command for dut in self.duts: virt_null = self._null_virtual_mac_command(dut) dut.config([virt_null]) response = dut.api('varp').get() self.assertIsNotNone(response) self.assertIn(response['mac_address'], [None, '00:00:00:00:00:00']) def test_set_mac_address_with_value(self): for dut in self.duts: virt_null = self._null_virtual_mac_command(dut) dut.config([virt_null]) api = dut.api('varp') self.assertNotIn(VIRT_ENTRY_A, api.config) result = dut.api('varp').set_mac_address('00:11:22:33:44:55') self.assertTrue(result) self.assertIn(VIRT_ENTRY_A, api.config) def test_change_mac_address(self): for dut in self.duts: virt_null = self._null_virtual_mac_command(dut) dut.config([virt_null, VIRT_ENTRY_A]) api = dut.api('varp') self.assertIn(VIRT_ENTRY_A, api.config) result = dut.api('varp').set_mac_address('00:11:22:33:44:56') self.assertTrue(result) self.assertIn(VIRT_ENTRY_B, api.config) def test_remove_mac_address(self): for dut in self.duts: virt_null = self._null_virtual_mac_command(dut) dut.config([virt_null, VIRT_ENTRY_A]) api = dut.api('varp') self.assertIn(VIRT_ENTRY_A, api.config) result = dut.api('varp').set_mac_address(disable=True) self.assertTrue(result) self.assertNotIn(VIRT_ENTRY_A, api.config) def test_set_mac_address_with_bad_value(self): for dut in self.duts: virt_null = self._null_virtual_mac_command(dut) dut.config([virt_null]) api = dut.api('varp') self.assertNotIn(VIRT_ENTRY_A, api.config) with self.assertRaises(ValueError): dut.api('varp').set_mac_address('0011.2233.4455') class TestApiVarpInterfaces(DutSystemTest): def test_set_virtual_addr_with_values_clean(self): for dut in self.duts: dut.config(['no interface Vlan1000', 'interface Vlan1000', 'ip address 1.1.1.1/24']) api = dut.api('varp') self.assertNotIn('ip virtual-router address 1.1.1.2', api.get_block('interface Vlan1000')) result = dut.api('varp').interfaces.set_addresses('Vlan1000', ['1.1.1.2', '1.1.1.3']) self.assertTrue(result) self.assertIn('ip virtual-router address 1.1.1.2', api.get_block('interface Vlan1000')) self.assertIn('ip virtual-router address 1.1.1.3', api.get_block('interface Vlan1000')) def test_set_virtual_addr_with_values_dirty(self): for dut in self.duts: dut.config(['no interface Vlan1000', 'interface Vlan1000', 'ip address 1.1.1.1/24', 'ip virtual-router address 1.1.1.20']) api = dut.api('varp') self.assertIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) result = dut.api('varp').interfaces.set_addresses('Vlan1000', ['1.1.1.2', '1.1.1.3']) self.assertTrue(result) self.assertIn('ip virtual-router address 1.1.1.2', api.get_block('interface Vlan1000')) self.assertIn('ip virtual-router address 1.1.1.3', api.get_block('interface Vlan1000')) self.assertNotIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) def test_default_virtual_addrs(self): for dut in self.duts: dut.config(['no interface Vlan1000', 'interface Vlan1000', 'ip address 1.1.1.1/24', 'ip virtual-router address 1.1.1.20', 'ip virtual-router address 1.1.1.21']) api = dut.api('varp') self.assertIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) result = dut.api('varp').interfaces.set_addresses('Vlan1000', default=True) self.assertTrue(result) self.assertNotIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertNotIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) def test_negate_virtual_addrs(self): for dut in self.duts: dut.config(['no interface Vlan1000', 'interface Vlan1000', 'ip address 1.1.1.1/24', 'ip virtual-router address 1.1.1.20', 'ip virtual-router address 1.1.1.21']) api = dut.api('varp') self.assertIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) result = dut.api('varp').interfaces.set_addresses('Vlan1000', addresses=None) self.assertTrue(result) self.assertNotIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertNotIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) def test_negate_virtual_addrs_with_disable(self): for dut in self.duts: dut.config(['no interface Vlan1000', 'interface Vlan1000', 'ip address 1.1.1.1/24', 'ip virtual-router address 1.1.1.20', 'ip virtual-router address 1.1.1.21']) api = dut.api('varp') self.assertIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) result = dut.api('varp').interfaces.set_addresses('Vlan1000', disable=True) self.assertTrue(result) self.assertNotIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertNotIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) def test_empty_list_virtual_addrs(self): for dut in self.duts: dut.config(['no interface Vlan1000', 'interface Vlan1000', 'ip address 1.1.1.1/24', 'ip virtual-router address 1.1.1.20', 'ip virtual-router address 1.1.1.21']) api = dut.api('varp') self.assertIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) result = dut.api('varp').interfaces.set_addresses('Vlan1000', addresses=[]) self.assertTrue(result) self.assertNotIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertNotIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) def test_no_attr_virtual_addrs(self): for dut in self.duts: dut.config(['no interface Vlan1000', 'interface Vlan1000', 'ip address 1.1.1.1/24', 'ip virtual-router address 1.1.1.20', 'ip virtual-router address 1.1.1.21']) api = dut.api('varp') self.assertIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) result = dut.api('varp').interfaces.set_addresses('Vlan1000') self.assertTrue(result) self.assertNotIn('ip virtual-router address 1.1.1.20', api.get_block('interface Vlan1000')) self.assertNotIn('ip virtual-router address 1.1.1.21', api.get_block('interface Vlan1000')) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_vlans.py0000644000076500000240000002062214447405743022045 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import random_int, random_string from systestlib import DutSystemTest class TestApiVlans(DutSystemTest): def test_get(self): for dut in self.duts: dut.config(['no vlan 1', 'vlan 1']) response = dut.api('vlans').get('1') values = dict(vlan_id='1', name='default', state='active', trunk_groups=[]) self.assertEqual(values, response) def test_getall(self): for dut in self.duts: dut.config(['no vlan 1-4094', 'vlan 1', 'vlan 10']) response = dut.api('vlans').getall() self.assertIsInstance(response, dict, 'dut=%s' % dut) for vid in ['1', '10']: self.assertIn(vid, response, 'dut=%s' % dut) def test_create_and_return_true(self): for dut in self.duts: dut.config('no vlan 2-4094') vid = str(random_int(2, 4094)) result = dut.api('vlans').create(vid) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan') self.assertIn(vid, config[0]['vlans'], 'dut=%s' % dut) def test_create_and_return_false(self): for dut in self.duts: result = dut.api('vlans').create('5000') self.assertFalse(result, 'dut=%s' % dut) def test_delete_and_return_true(self): for dut in self.duts: vid = str(random_int(2, 4094)) dut.config('vlan %s' % vid) result = dut.api('vlans').delete(vid) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan') self.assertNotIn(vid, config[0]['vlans']) def test_delete_and_return_false(self): for dut in self.duts: result = dut.api('vlans').delete('5000') self.assertFalse(result, 'dut=%s' % dut) def test_default(self): for dut in self.duts: vid = str(random_int(2, 4095)) name = random_string(maxchar=20) dut.config(['no vlan %s' % vid, 'vlan %s' % vid, 'vlan %s' % vid, 'name %s' % name]) result = dut.api('vlans').default(vid) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan') self.assertNotIn(vid, config[0]['vlans'], 'dut=%s' % dut) def test_set_name(self): for dut in self.duts: name = random_string(maxchar=20) vid = str(random_int(2, 4095)) dut.config(['no vlan %s' % vid, 'vlan %s' % vid]) result = dut.api('vlans').set_name(vid, name) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan') self.assertEqual(name, config[0]['vlans'][vid]['name'], 'dut=%s' % dut) def test_set_state_active(self): for dut in self.duts: vid = str(random_int(2, 4095)) dut.config(['no vlan %s' % vid, 'vlan %s' % vid, 'state suspend']) result = dut.api('vlans').set_state(vid, 'active') self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan') self.assertEqual('active', config[0]['vlans'][vid]['status'], 'dut=%s' % dut) def test_set_state_suspend(self): for dut in self.duts: vid = str(random_int(2, 4095)) dut.config(['no vlan %s' % vid, 'vlan %s' % vid, 'state active']) result = dut.api('vlans').set_state(vid, 'suspend') self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan') self.assertEqual('suspended', config[0]['vlans'][vid]['status'], 'dut=%s' % dut) def test_set_trunk_groups_default(self): for dut in self.duts: vid = str(random_int(2, 4094)) tg = random_string() dut.config(['no vlan %s' % vid, 'vlan %s' % vid, 'trunk group %s' % tg]) result = dut.api('vlans').set_trunk_groups(vid, default=True) self.assertTrue(result, 'dut=%s' % dut) cmd = 'show running-config section vlan %s' % vid config = dut.run_commands(cmd, 'text') self.assertNotIn('trunk group', config[0]['output']) def test_set_trunk_groups(self): for dut in self.duts: vid = str(random_int(2, 4094)) tg1 = random_string(maxchar=10) tg2 = random_string(maxchar=10) tg3 = random_string(maxchar=10) dut.config(['no vlan %s' % vid, 'vlan %s' % vid, 'trunk group %s' % tg1, 'trunk group %s' % tg2]) result = dut.api('vlans').set_trunk_groups(vid, [tg1, tg3]) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan %s trunk group' % vid) config = sorted(config[0]['trunkGroups'][vid]['names']) self.assertEqual(sorted([tg1, tg3]), config) def test_set_trunk_groups_disable(self): for dut in self.duts: vid = str(random_int(2, 4094)) tg1 = random_string(maxchar=10) tg2 = random_string(maxchar=10) tg3 = random_string(maxchar=10) dut.config(['no vlan %s' % vid, 'vlan %s' % vid, 'trunk group %s' % tg1, 'trunk group %s' % tg2, 'trunk group %s' % tg3]) result = dut.api('vlans').set_trunk_groups(vid, disable=True) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan %s trunk group' % vid) config = sorted(config[0]['trunkGroups'][vid]['names']) self.assertEqual([], config) def test_add_trunk_group(self): for dut in self.duts: tg = random_string(maxchar=32) vid = str(random_int(2, 4095)) dut.config(['no vlan %s' % vid, 'vlan %s' % vid, 'no trunk group']) result = dut.api('vlans').add_trunk_group(vid, tg) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan trunk group') self.assertIn(tg, config[0]['trunkGroups'][vid]['names'], 'dut=%s' % dut) def test_remove_trunk_group(self): for dut in self.duts: tg = random_string(maxchar=32) vid = str(random_int(2, 4095)) dut.config(['no vlan %s' % vid, 'vlan %s' % vid, 'no trunk group', 'trunk group %s' % tg]) result = dut.api('vlans').remove_trunk_group(vid, tg) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vlan trunk group') self.assertNotIn(tg, config[0]['trunkGroups'][vid]['names'], 'dut=%s' % dut) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_vrfs.py0000644000076500000240000003502314447405743021703 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2017, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import random_string from systestlib import DutSystemTest from pyeapi.utils import CliVariants class TestApiVrfs(DutSystemTest): def test_get(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah', 'rd 10:10', 'description blah desc']) else: dut.config(['no vrf definition blah', 'vrf definition blah', 'rd 10:10', 'description blah desc']) response = dut.api('vrfs').get('blah') values = dict(rd='10:10', vrf_name='blah', description='blah desc', ipv4_routing=False, ipv6_routing=False) self.assertEqual(values, response) if dut.version_number >= '4.23': dut.config(['no vrf instance blah']) else: dut.config(['no vrf definition blah']) def test_getall(self): for dut in self.duts: dut.config( CliVariants( ['no vrf instance blah', 'vrf instance blah', 'no vrf instance second', 'vrf instance second'], ['no vrf definition blah', 'vrf definition blah', 'no vrf definition second', 'vrf definition second']) ) response = dut.api('vrfs').getall() self.assertIsInstance( response, dict, f'dut={dut}' ) self.assertEqual( len(response), 2 ) for vrf_name in ( 'blah', 'second' ): self.assertIn( vrf_name, response, f'dut={dut}' ) dut.config( CliVariants( ['no vrf instance blah', 'no vrf instance second'], ['no vrf definition blah', 'no vrf definition second']) ) def test_create_and_return_true(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah']) else: dut.config(['no vrf definition blah', 'vrf definition blah']) result = dut.api('vrfs').create('blah') self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands('show vrf', encoding='text') self.assertIn('blah', config[0]['output'], 'dut=%s' % dut) if dut.version_number >= '4.23': dut.config(['no vrf instance blah']) else: dut.config(['no vrf definition blah']) def test_create_with_valid_rd(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah']) else: dut.config(['no vrf definition blah', 'vrf definition blah']) result = dut.api('vrfs').create('blah', rd='10:10') self.assertTrue(result, 'dut=%s' % dut) command = 'show running-config section vrf' config = dut.run_commands(command, encoding='text') if dut.version_number >= '4.23': self.assertIn('vrf instance blah', config[0]['output'], 'dut=%s' % dut) else: self.assertIn('vrf definition blah', config[0]['output'], 'dut=%s' % dut) self.assertIn('rd 10:10', config[0]['output'], 'dut=%s' % dut) if dut.version_number >= '4.23': dut.config(['no vrf instance blah']) else: dut.config(['no vrf definition blah']) def test_create_and_return_false(self): for dut in self.duts: result = dut.api('vrfs').create('a%') self.assertFalse(result, 'dut=%s' % dut) def test_create_with_invalid_rd(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah']) else: dut.config(['no vrf definition blah', 'vrf definition blah']) result = dut.api('vrfs').create('blah', rd='192.168.1.1:99999999') self.assertFalse(result, 'dut=%s' % dut) command = 'show running-config section vrf' config = dut.run_commands(command, encoding='text') if dut.version_number >= '4.23': self.assertIn('vrf instance blah', config[0]['output'], 'dut=%s' % dut) else: self.assertIn('vrf definition blah', config[0]['output'], 'dut=%s' % dut) self.assertNotIn('rd', config[0]['output'], 'dut=%s' % dut) if dut.version_number >= '4.23': dut.config(['no vrf instance blah']) else: dut.config(['no vrf definition blah']) def test_delete_and_return_true(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config('vrf instance blah') else: dut.config('vrf definition blah') result = dut.api('vrfs').delete('blah') self.assertTrue(result, 'dut=%s' % dut) command = 'show running-config section vrf' config = dut.run_commands(command, encoding='text') if dut.version_number >= '4.23': self.assertNotIn('vrf instance blah', config[0]['output'], 'dut=%s' % dut) else: self.assertNotIn('vrf definition blah', config[0]['output'], 'dut=%s' % dut) def test_delete_and_return_false(self): for dut in self.duts: result = dut.api('vrfs').delete('a%') self.assertFalse(result, 'dut=%s' % dut) def test_default(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah', 'description test desc']) else: dut.config(['no vrf definition blah', 'vrf definition blah', 'description test desc']) result = dut.api('vrfs').default('blah') self.assertTrue(result, 'dut=%s' % dut) command = 'show running-config section vrf' config = dut.run_commands(command, encoding='text') if dut.version_number >= '4.23': self.assertNotIn('vrf instance blah', config[0]['output'], 'dut=%s' % dut) dut.config(['no vrf instance blah']) else: self.assertNotIn('vrf definition blah', config[0]['output'], 'dut=%s' % dut) dut.config(['no vrf definition blah']) def test_set_rd(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah']) else: dut.config(['no vrf definition blah', 'vrf definition blah']) result = dut.api('vrfs').set_rd('blah', '10:10') self.assertTrue(result, 'dut=%s' % dut) command = 'show running-config section vrf' config = dut.run_commands(command, encoding='text') self.assertIn('blah', config[0]['output'], 'dut=%s' % dut) self.assertIn('10:10', config[0]['output'], 'dut=%s' % dut) if dut.version_number >= '4.23': dut.config(['no vrf instance blah']) else: dut.config(['no vrf definition blah']) def test_set_description(self): for dut in self.duts: description = random_string() if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah']) else: dut.config(['no vrf definition blah', 'vrf definition blah']) result = dut.api('vrfs').set_description('blah', description) self.assertTrue(result, 'dut=%s' % dut) command = 'show running-config section vrf' config = dut.run_commands(command, encoding='text') self.assertIn('description %s' % description, config[0]['output'], 'dut=%s' % dut) result = dut.api('vrfs').set_description('blah', default=True) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands(command, encoding='text') self.assertNotIn('description %s' % description, config[0]['output'], 'dut=%s' % dut) if dut.version_number >= '4.23': dut.config(['no vrf instance blah']) else: dut.config(['no vrf definition blah']) def test_set_ipv4_routing(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah', 'rd 10:10', 'description test']) else: dut.config(['no vrf definition blah', 'vrf definition blah', 'rd 10:10', 'description test']) result = dut.api('vrfs').set_ipv4_routing('blah') self.assertTrue(result, 'dut=%s' % dut) command = 'show running-config section vrf' config = dut.run_commands(command, encoding='text') self.assertIn('ip routing vrf blah', config[0]['output'], 'dut=%s' % dut) result = dut.api('vrfs').set_ipv4_routing('blah', default=True) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands(command, encoding='text') self.assertIn('no ip routing vrf blah', config[0]['output'], 'dut=%s' % dut) if dut.version_number >= '4.23': dut.config(['no vrf instance blah']) else: dut.config(['no vrf definition blah']) def test_set_ipv6_routing(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah', 'rd 10:10', 'description test']) else: dut.config(['no vrf definition blah', 'vrf definition blah', 'rd 10:10', 'description test']) result = dut.api('vrfs').set_ipv6_routing('blah') self.assertTrue(result, 'dut=%s' % dut) command = 'show running-config all section vrf' config = dut.run_commands(command, encoding='text') self.assertIn('ipv6 unicast-routing vrf blah', config[0]['output'], 'dut=%s' % dut) result = dut.api('vrfs').set_ipv6_routing('blah', default=True) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands(command, encoding='text') self.assertIn('no ipv6 unicast-routing vrf blah', config[0]['output'], 'dut=%s' % dut) if dut.version_number >= '4.23': dut.config(['no vrf instance blah']) else: dut.config(['no vrf definition blah']) def test_set_interface(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config(['no vrf instance blah', 'vrf instance blah', 'rd 10:10', 'default interface Ethernet1', 'interface Ethernet1', 'no switchport']) else: dut.config(['no vrf definition blah', 'vrf definition blah', 'rd 10:10', 'default interface Ethernet1', 'interface Ethernet1', 'no switchport']) result = dut.api('vrfs').set_interface('blah', 'Ethernet1') self.assertTrue(result, 'dut=%s' % dut) command = 'show running-config interfaces Ethernet1' config = dut.run_commands(command, encoding='text') if dut.version_number >= '4.23': self.assertIn('vrf blah', config[0]['output'], 'dut=%s' % dut) else: self.assertIn('vrf forwarding blah', config[0]['output'], 'dut=%s' % dut) result = dut.api('vrfs').set_interface('blah', 'Ethernet1', disable=True) self.assertTrue(result, 'dut=%s' % dut) config = dut.run_commands(command, encoding='text') if dut.version_number >= '4.23': self.assertNotIn('vrf blah', config[0]['output'], 'dut=%s' % dut) dut.config(['no vrf instance blah', 'default interface Ethernet1']) else: self.assertNotIn('vrf forwarding blah', config[0]['output'], 'dut=%s' % dut) dut.config(['no vrf definition blah', 'default interface Ethernet1']) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_api_vrrp.py0000644000076500000240000005435714447405743021727 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from systestlib import DutSystemTest IP_PREFIX = '10.10.10.' VR_CONFIG = { 'primary_ip': '10.10.10.2', 'priority': 200, 'description': 'modified vrrp 10 on an interface', 'secondary_ip': ['10.10.10.11'], 'ip_version': 3, 'enable': False, 'timers_advertise': 2, 'mac_addr_adv_interval': 3, 'preempt': False, 'preempt_delay_min': 1, 'preempt_delay_reload': None, 'delay_reload': 1, 'track': [ {'name': 'Ethernet1', 'action': 'shutdown'}, {'name': 'Ethernet2', 'action': 'decrement', 'amount': 10}, {'name': 'Ethernet2', 'action': 'shutdown'}, ], 'bfd_ip': '10.10.10.150', } class TestApiVrrp(DutSystemTest): def _vlan_setup(self, dut): dut.config(['no interface vlan 101', 'interface vlan 101', 'ip address %s1/24' % IP_PREFIX, 'exit']) return 'Vlan101' def test_get(self): vrid = 98 for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'vrrp %d shutdown' % vrid, 'exit']) response = dut.api('vrrp').get(interface) self.assertIsNotNone(response) def test_getall(self): vrid = 98 vrid2 = 198 for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'vrrp %d disabled' % vrid, 'exit', 'interface vlan $', 'vrrp %d disabled' % vrid, 'vrrp %d disabled' % vrid2, 'exit']) else: dut.config(['interface %s' % interface, 'vrrp %d shutdown' % vrid, 'exit', 'interface vlan $', 'vrrp %d shutdown' % vrid, 'vrrp %d shutdown' % vrid2, 'exit']) response = dut.api('vrrp').getall() self.assertIsNotNone(response) def test_create(self): vrid = 99 import copy vrrp_conf = copy.deepcopy(VR_CONFIG) # vrrp_conf = dict(VR_CONFIG) for dut in self.duts: interface = self._vlan_setup(dut) dut.config([f'interface {interface}', f'no vrrp {vrid}', 'exit']) response = dut.api('vrrp').create(interface, vrid, **vrrp_conf) self.assertIs(response, True) # Fix the configuration dict for proper output vrrp_conf = dut.api('vrrp').vrconf_format(vrrp_conf) response = dut.api('vrrp').get(interface)[vrid] self.maxDiff = None # now delete dict items which vary between versions for key in ( 'preempt_delay_min', 'preempt_delay_reload' ): del vrrp_conf[ key ], response[ key ] self.assertEqual(response, vrrp_conf) def test_delete(self): vrid = 101 for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) response = dut.api('vrrp').delete(interface, vrid) self.assertIs(response, True) def test_default(self): vrid = 102 for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) response = dut.api('vrrp').delete(interface, vrid) self.assertIs(response, True) def test_update_with_create(self): vrid = 103 import copy vrrp_conf = copy.deepcopy(VR_CONFIG) # vrrp_conf = dict(VR_CONFIG) for dut in self.duts: interface = self._vlan_setup(dut) dut.config([f'interface {interface}', f'no vrrp {vrid}', 'exit']) # Create the inital vrrp on the interface response = dut.api('vrrp').create(interface, vrid, **vrrp_conf) self.assertIs(response, True) # Update some of the information on the vrrp vrrp_update = { 'primary_ip': '10.10.10.12', 'priority': 200, 'description': 'updated vrrp 10 on an interface', 'secondary_ip': ['10.10.10.13', '10.10.10.23'], 'ip_version': 2, 'enable': True, 'timers_advertise': None, 'mac_addr_adv_interval': 'default', 'preempt': True, 'preempt_delay_min': 'default', 'preempt_delay_reload': 'default', 'delay_reload': 'default', 'track': [ {'name': 'Ethernet2', 'action': 'shutdown'}, {'name': 'Ethernet2', 'action': 'decrement', 'amount': 1}, ], 'bfd_ip': None, } response = dut.api('vrrp').create(interface, vrid, **vrrp_update) self.assertIs(response, True) vrrp_update = dut.api('vrrp').vrconf_format(vrrp_update) response = dut.api('vrrp').get(interface)[vrid] # now delete dict items which vary between versions for key in ( 'preempt_delay_min', 'preempt_delay_reload' ): del vrrp_update[ key ], response[ key ] self.maxDiff = None self.assertEqual(response, vrrp_update) def test_set_enable(self): vrid = 104 enable_cases = [ {'value': True}, {'value': False}, {'value': True}, {'value': False}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'vrrp %d ipv4 10.10.10.2' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'vrrp %d ip 10.10.10.2' % vrid, 'exit']) for enable in enable_cases: response = dut.api('vrrp').set_enable( interface, vrid, **enable) self.assertIs(response, True) def test_set_primary_ip(self): vrid = 104 primary_ip_cases = [ {'value': '10.10.10.2'}, {'default': True}, {'value': '10.10.10.3'}, {'disable': True}, {'value': '10.10.10.4'}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for p_ip in primary_ip_cases: response = dut.api('vrrp').set_primary_ip( interface, vrid, **p_ip) self.assertIs(response, True) def test_set_priority(self): vrid = 104 priority_cases = [ {'value': 200}, {'default': True}, {'value': 175}, {'disable': True}, {'value': 190} ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for priority in priority_cases: response = dut.api('vrrp').set_priority( interface, vrid, **priority) self.assertIs(response, True) def test_set_description(self): vrid = 104 desc_cases = [ {'value': '1st modified vrrp'}, {'default': True}, {'value': '2nd modified vrrp'}, {'disable': True}, {'value': '3rd modified vrrp'}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for description in desc_cases: response = dut.api('vrrp').set_description( interface, vrid, **description) self.assertIs(response, True) def test_set_secondary_ips(self): vrid = 104 secondary_ip_cases = [ ['10.10.10.51', '10.10.10.52'], ['10.10.10.53', '10.10.10.54'], [], ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for s_ip_list in secondary_ip_cases: response = dut.api('vrrp').set_secondary_ips( interface, vrid, s_ip_list) self.assertIs(response, True) def test_set_ip_version(self): vrid = 104 ip_version_cases = [ {'value': 2}, {'value': 3}, {'default': True}, {'value': 3}, {'disable': True}, {'value': 3}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for ip_version in ip_version_cases: response = dut.api('vrrp').set_ip_version( interface, vrid, **ip_version) self.assertIs(response, True) def test_set_timers_advertise(self): vrid = 104 timers_adv_cases = [ {'value': 10}, {'default': True}, {'value': 20}, {'disable': True}, {'value': 30}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for timers_advertise in timers_adv_cases: response = dut.api('vrrp').set_timers_advertise( interface, vrid, **timers_advertise) self.assertIs(response, True) def test_set_mac_addr_adv_interval(self): vrid = 104 mac_addr_adv_int_cases = [ {'value': 50}, {'default': True}, {'value': 55}, {'disable': True}, {'value': 60}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for mac_addr_adv_intvl in mac_addr_adv_int_cases: response = dut.api('vrrp').set_mac_addr_adv_interval( interface, vrid, **mac_addr_adv_intvl) self.assertIs(response, True) def test_set_preempt(self): vrid = 104 preempt_cases = [ {'value': True}, {'default': True}, {'value': True}, {'disable': True}, {'value': True}, {'value': False}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for preempt in preempt_cases: response = dut.api('vrrp').set_preempt( interface, vrid, **preempt) self.assertIs(response, True) def test_set_preempt_delay_min(self): vrid = 104 preempt_delay_min_cases = [ {'value': 3600}, {'default': True}, {'value': 500}, {'disable': True}, {'value': 150}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for preempt_delay_min in preempt_delay_min_cases: response = dut.api('vrrp').set_preempt_delay_min( interface, vrid, **preempt_delay_min) self.assertIs(response, True) def test_set_preempt_delay_reload(self): vrid = 104 preempt_delay_reload_cases = [ {'value': 3600}, {'default': True}, {'value': 500}, {'disable': True}, {'value': 150}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for preempt_delay_reload in preempt_delay_reload_cases: response = dut.api('vrrp').set_preempt_delay_reload( interface, vrid, **preempt_delay_reload) self.assertIs(response, True) def test_set_delay_reload(self): vrid = 104 delay_reload_cases = [ {'value': 30}, {'default': True}, {'value': 25}, {'disable': True}, {'value': 15}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for delay_reload in delay_reload_cases: response = dut.api('vrrp').set_delay_reload( interface, vrid, **delay_reload) self.assertIs(response, True) def test_set_tracks(self): vrid = 104 track_cases = [ [ {'name': 'Ethernet1', 'action': 'shutdown'}, {'name': 'Ethernet2', 'action': 'decrement', 'amount': 10}, {'name': 'Ethernet2', 'action': 'shutdown'}, ], [ {'name': 'Ethernet1', 'action': 'shutdown'}, ], [ {'name': 'Ethernet1', 'action': 'shutdown'}, {'name': 'Ethernet2', 'action': 'decrement', 'amount': 20}, {'name': 'Ethernet2', 'action': 'shutdown'}, ], [ {'name': 'Ethernet1', 'action': 'shutdown'}, ], [], ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for track_list in track_cases: response = dut.api('vrrp').set_tracks( interface, vrid, track_list) self.assertIs(response, True) def test_set_bfd_ip(self): vrid = 104 bfd_ip_cases = [ {'value': '10.10.10.160'}, {'default': True}, {'value': '10.10.10.161'}, {'disable': True}, {'value': '10.10.10.162'}, ] for dut in self.duts: interface = self._vlan_setup(dut) if dut.version_number >= '4.23': dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d disabled' % vrid, 'exit']) else: dut.config(['interface %s' % interface, 'no vrrp %d' % vrid, 'vrrp %d shutdown' % vrid, 'exit']) for bfd_ip in bfd_ip_cases: response = dut.api('vrrp').set_bfd_ip( interface, vrid, **bfd_ip) self.assertIs(response, True) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/system/test_client.py0000644000076500000240000003506314447405743021354 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import random_int, random_string, get_fixture from unittest.mock import patch import pyeapi.client import pyeapi.eapilib class TestClient(unittest.TestCase): def setUp(self): pyeapi.client.load_config(filename=get_fixture('dut.conf')) config = pyeapi.client.config self.duts = list() for name in config.sections(): if name.startswith('connection:') and 'localhost' not in name: name = name.split(':')[1] dut = pyeapi.client.connect_to(name) self.duts.append(dut) if dut._enablepwd is not None: # If enable password defined for dut, set the # enable password on the dut and clear it on tearDown if dut.version_number >= '4.23': dut.config("enable password %s" % dut._enablepwd) else: dut.config("enable secret %s" % dut._enablepwd) def test_unauthorized_user(self): error_string = ('Unauthorized. Unable to authenticate user: Bad' ' username/password combination') for dut in self.duts: temp_node = pyeapi.connect(host=dut.settings['host'], transport=dut.settings['transport'], username='wrong', password='nope', port=dut.settings['port'], return_node=True) try: temp_node.run_commands('show version') except pyeapi.eapilib.ConnectionError as err: self.assertEqual(err.message, error_string) def test_populate_version_properties(self): for dut in self.duts: result = dut.run_commands('show version') self.assertEqual(dut.version, result[0]['version']) self.assertIn(dut.model, result[0]['modelName']) self.assertIn(dut.version_number, result[0]['version']) def test_enable_single_command(self): for dut in self.duts: result = dut.run_commands('show version') self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), 1, 'dut=%s' % dut) def test_enable_single_extended_command(self): for dut in self.duts: result = dut.run_commands({'cmd': 'show cvx', 'revision': 1}) self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), 1, 'dut=%s' % dut) self.assertTrue('clusterMode' not in result[0].keys()) result2 = dut.run_commands({'cmd': 'show cvx', 'revision': 2}) self.assertIsInstance(result2, list, 'dut=%s' % dut) self.assertEqual(len(result2), 1, 'dut=%s' % dut) self.assertTrue('clusterMode' in result2[0].keys()) def test_enable_single_unicode_command(self): for dut in self.duts: result = dut.run_commands(u'show version') self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), 1, 'dut=%s' % dut) def test_no_enable_single_command(self): for dut in self.duts: result = dut.run_commands('show version', 'json', send_enable=False) self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), 1, 'dut=%s' % dut) def test_no_enable_single_command_no_auth(self): for dut in self.duts: with self.assertRaises(pyeapi.eapilib.CommandError): dut.run_commands(['disable', 'show running-config'], 'json', send_enable=False) def test_enable_multiple_commands(self): for dut in self.duts: commands = list() for i in range(1, random_int(10, 200)): commands.append('show version') result = dut.run_commands(commands[:]) self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), len(commands), 'dut=%s' % dut) def test_enable_multiple_unicode_commands(self): for dut in self.duts: commands = list() for i in range(1, random_int(10, 200)): commands.append(u'show version') result = dut.enable(commands[:]) self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), len(commands), 'dut=%s' % dut) def test_config_single_command(self): for dut in self.duts: hostname = 'hostname %s' % random_string(5, 50) result = dut.config(hostname) self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), 1, 'dut=%s' % dut) self.assertEqual(result[0], {}, 'dut=%s' % dut) result = dut.run_commands('show running-config | include %s$' % hostname, 'text') self.assertEqual(result[0]['output'].strip(), hostname) def test_config_single_multiline_command(self): for dut in self.duts: # Clear any current banner dut.config('no banner login') banner = 'This is a new banner\nwith different lines!!!' cmd = 'banner login MULTILINE:%s' % banner result = dut.config(cmd) self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), 1, 'dut=%s' % dut) self.assertEqual(result[0], {}, 'dut=%s' % dut) result = dut.run_commands('show banner login', 'text') self.assertEqual(result[0]['output'].strip().split('\n'), banner.split('\n')) def test_config_multiple_commands(self): for dut in self.duts: commands = list() for i in range(1, random_int(10, 200)): commands.append('hostname %s' % random_string(5, 20)) result = dut.config(commands[:]) self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), len(commands), 'dut=%s' % dut) def test_multiple_requests(self): for dut in self.duts: for i in range(1, random_int(10, 200)): result = dut.run_commands('show version') self.assertIsInstance(result, list, 'dut=%s' % dut) self.assertEqual(len(result), 1, 'dut=%s' % dut) def test_get_block(self): # Verify get_block using a config string returns correct value for dut in self.duts: api = dut.api('interfaces') config = api.config running = api.get_block('interface Ethernet1') txtstr = api.get_block('interface Ethernet1', config=config) self.assertEqual(running, txtstr) def test_get_block_none(self): # Verify get_block using a config string where match fails returns None for dut in self.duts: api = dut.api('interfaces') txtstr = api.get_block('interface Ethernet1', config='config') self.assertEqual(txtstr, None) def test_execute_with_autocomplete(self): # There are some versions of EOS before 4.17.x that have the # autocomplete feature available. If system tests are run on one of # those version of EOS this system test will fail. for dut in self.duts: version = self._dut_eos_version(dut) version = version.split('.') if int(version[0]) >= 4 and int(version[1]) >= 17: result = dut.connection.execute(['sh ver'], encoding='json', autoComplete=True) self.assertIn('version', result['result'][0]) else: # Verify exception thrown for EOS version that does not # support autoComplete parameter with EAPI with self.assertRaises(pyeapi.eapilib.CommandError): dut.connection.execute(['sh ver'], encoding='json', autoComplete=True) def test_execute_with_expandaliases(self): # There are some versions of EOS before 4.17.x that have the # expandaliases feature available. If system tests are run on one of # those version of EOS this system test will fail. for dut in self.duts: # configure an alias for show version command dut.config(['alias test show version']) version = self._dut_eos_version(dut) version = version.split('.') if int(version[0]) >= 4 and int(version[1]) >= 17: result = dut.connection.execute(['test'], encoding='json', expandAliases=True) self.assertIn('version', result['result'][0]) else: # Verify exception thrown for EOS version that does not # support expandAliases parameter with EAPI with self.assertRaises(pyeapi.eapilib.CommandError): dut.connection.execute(['test'], encoding='json', expandAliases=True) @patch('pyeapi.eapilib._LOGGER.exception') def test_execute_socket_timeout_error(self, logexception): for dut in self.duts: self.assertEqual(dut.connection.transport.timeout, 60) dut.connection.transport.timeout = 0.001 try: dut.connection.execute(['show version'], encoding='json') except pyeapi.eapilib.ConnectionError as err: error_msg = 'Socket error during eAPI connection: timed out' self.assertEqual(err.message, error_msg) logexception.assert_called_once() dut.connection.transport.timeout = 60 def _dut_eos_version(self, dut): result = dut.connection.execute(['show version'], encoding='json') return result['result'][0]['version'] def tearDown(self): for dut in self.duts: if dut.version_number >= '4.23': dut.config("no enable password") else: dut.config("no enable secret") class TestNode(unittest.TestCase): def setUp(self): pyeapi.client.load_config(filename=get_fixture('dut.conf')) config = pyeapi.client.config self.duts = list() for name in config.sections(): if name.startswith('connection:') and 'localhost' not in name: name = name.split(':')[1] self.duts.append(pyeapi.client.connect_to(name)) if not hasattr(self, 'assertRegex'): self.assertRegex = self.assertRegexpMatches def test_exception_trace(self): # Send commands that will return an error and validate the errors # General format of an error message: rfmt = r'Error \[%d\]: CLI command \d+ of \d+ \'[^\']*\' failed: %s\[%s\]' # Design error tests cases = [] # Send an incomplete command cases.append( ('show run', rfmt % (1002, r'invalid command \[[^[]+', r'"Incomplete token \(at token \d+:[^\)]+\)"'))) # Send a mangled command cases.append(('shwo version', rfmt % (1002, r'invalid command \[[^[]+', r'"Invalid input \(at token \d+:[^\)]+\)"'))) # Send a command that cannot be run through the api # note the command for reload looks to change in new EOS # in 4.15 the reload now is replaced with 'force' if you are # testing some DUT running older code and this test fails # change the error message to the following: # To reload the machine over the API, please use 'reload now' instead cases.append(('reload', rfmt % (1004, r'incompatible command \[[^[]+', r"'Command not permitted via API access\..*"))) # Send a command that has insufficient priv cases.append(('show running-config', rfmt % (1002, r'invalid command \[[^[]+', r"'Invalid input \(privileged mode required\)'"))) for dut in self.duts: for (cmd, regex) in cases: try: # Insert the error in list of valid commands if cmd != "show running-config": dut.enable(['show version', cmd, 'show hostname'], strict=True) else: dut.enable(['disable', 'show version', cmd], strict=True, send_enable=False) self.fail('A CommandError should have been raised') except pyeapi.eapilib.CommandError as exc: # Validate the properties of the exception self.assertEqual( len(exc.trace), 3 if cmd == 'show running-config' else 4 ) self.assertIsNotNone(exc.command_error) self.assertIsNotNone(exc.output) self.assertIsNotNone(exc.commands) self.assertRegex(exc.message, regex) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/0000755000076500000240000000000014447406213016102 5ustar dlyssenkostaff00000000000000pyeapi-1.0.2/test/unit/test_api_acl.py0000644000076500000240000002613114447405743021115 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.acl class TestApiAclFunctions(unittest.TestCase): def test_mask_to_prefixlen(self): result = pyeapi.api.acl.mask_to_prefixlen('255.255.255.0') self.assertEqual(result, 24) def test_prefixlen_to_mask(self): result = pyeapi.api.acl.prefixlen_to_mask(24) self.assertEqual(result, '255.255.255.0') class TestApiAcls(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiAcls, self).__init__(*args, **kwargs) self.instance = pyeapi.api.acl.Acls(None) self.config = open(get_fixture('running_config.text')).read() def test_instance(self): result = pyeapi.api.acl.instance(None) self.assertIsInstance(result, pyeapi.api.acl.Acls) def test_getall(self): result = self.instance.getall() self.assertIsInstance(result, dict) self.assertIn('exttest', result['extended']) self.assertIn('test', result['standard']) def test_get_not_configured(self): self.assertIsNone(self.instance.get('unconfigured')) def test_get(self): result = self.instance.get('test') keys = ['name', 'type', 'entries'] self.assertEqual(sorted(keys), sorted(result.keys())) def test_get_instance(self): result = self.instance.get_instance('test') self.assertIsInstance(result, pyeapi.api.acl.StandardAcls) self.instance._instances['test'] = result result = self.instance.get_instance('exttest') self.assertIsInstance(result, pyeapi.api.acl.ExtendedAcls) result = self.instance.get_instance('unconfigured') self.assertIsInstance(result, dict) self.assertIsNone(result['unconfigured']) result = self.instance.get_instance('test') self.assertIsInstance(result, pyeapi.api.acl.StandardAcls) self.assertEqual(len(self.instance._instances), 2) def test_create_instance_standard(self): result = self.instance.create_instance('test', 'standard') self.assertIsInstance(result, pyeapi.api.acl.StandardAcls) self.assertEqual(len(self.instance._instances), 1) def test_create_instance_extended(self): result = self.instance.create_instance('exttest', 'extended') self.assertIsInstance(result, pyeapi.api.acl.ExtendedAcls) self.assertEqual(len(self.instance._instances), 1) def test_create_standard(self): cmds = 'ip access-list standard test' func = function('create', 'test') self.eapi_positive_config_test(func, cmds) def test_create_extended(self): cmds = 'ip access-list exttest' func = function('create', 'exttest', 'extended') self.eapi_positive_config_test(func, cmds) def test_create_unknown_type_creates_standard(self): cmds = 'ip access-list standard test' func = function('create', 'test', 'unknown') self.eapi_positive_config_test(func, cmds) def test_proxy_method_success(self): result = self.instance.remove_entry('test', '10') self.assertTrue(result) def test_proxy_method_raises_attribute_error(self): with self.assertRaises(AttributeError): self.instance.nonmethod('test', '10') class TestApiStandardAcls(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiStandardAcls, self).__init__(*args, **kwargs) self.instance = pyeapi.api.acl.StandardAcls(None) self.config = open(get_fixture('running_config.text')).read() def test_get(self): result = self.instance.get('test') keys = ['name', 'type', 'entries'] self.assertEqual(sorted(keys), sorted(result.keys())) self.assertEqual(result['type'], 'standard') def test_get_not_configured(self): self.assertIsNone(self.instance.get('unconfigured')) def test_acl_functions(self): for name in ['create', 'delete', 'default']: if name == 'create': cmds = 'ip access-list standard test' elif name == 'delete': cmds = 'no ip access-list standard test' elif name == 'default': cmds = 'default ip access-list standard test' func = function(name, 'test') self.eapi_positive_config_test(func, cmds) def test_update_entry(self): cmds = ['ip access-list standard test', 'no 10', '10 permit 0.0.0.0/32 log', 'exit'] func = function('update_entry', 'test', '10', 'permit', '0.0.0.0', '32', True) self.eapi_positive_config_test(func, cmds) def test_update_entry_no_log(self): cmds = ['ip access-list standard test', 'no 10', '10 permit 0.0.0.0/32', 'exit'] func = function('update_entry', 'test', '10', 'permit', '0.0.0.0', '32') self.eapi_positive_config_test(func, cmds) def test_remove_entry(self): cmds = ['ip access-list standard test', 'no 10', 'exit'] func = function('remove_entry', 'test', '10') self.eapi_positive_config_test(func, cmds) def test_add_entry(self): cmds = ['ip access-list standard test', 'permit 0.0.0.0/32 log', 'exit'] func = function('add_entry', 'test', 'permit', '0.0.0.0', '32', True) self.eapi_positive_config_test(func, cmds) def test_add_entry_no_log(self): cmds = ['ip access-list standard test', 'permit 0.0.0.0/32', 'exit'] func = function('add_entry', 'test', 'permit', '0.0.0.0', '32') self.eapi_positive_config_test(func, cmds) def test_add_entry_with_seqno(self): cmds = ['ip access-list standard test', '30 permit 0.0.0.0/32 log', 'exit'] func = function('add_entry', 'test', 'permit', '0.0.0.0', '32', True, 30) self.eapi_positive_config_test(func, cmds) class TestApiExtendedAcls(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiExtendedAcls, self).__init__(*args, **kwargs) self.instance = pyeapi.api.acl.ExtendedAcls(None) self.config = open(get_fixture('running_config.text')).read() def test_get(self): result = self.instance.get('exttest') keys = ['name', 'type', 'entries'] self.assertEqual(sorted(keys), sorted(result.keys())) self.assertEqual(result['type'], 'extended') self.assertIn('entries', result) self.assertIn('50', result['entries']) entry = dict(action='permit', dstaddr='1.1.1.2', dstlen=None, dstport=None, other=None, protocol='ip', srcaddr='any', srclen=None, srcport=None) self.assertEqual(entry, result['entries']['50']) self.assertIn('70', result['entries']) entry = dict(action='permit', dstaddr='3.3.3.3', dstlen=None, dstport='lt ipp', protocol='tcp', srcaddr='8.8.8.0', other='urg ttl eq 24 fragments tracked log', srclen='24', srcport='neq irc') self.assertEqual(entry, result['entries']['70']) def test_get_not_configured(self): self.assertIsNone(self.instance.get('unconfigured')) def test_acl_functions(self): for name in ['create', 'delete', 'default']: if name == 'create': cmds = 'ip access-list exttest' elif name == 'delete': cmds = 'no ip access-list exttest' elif name == 'default': cmds = 'default ip access-list exttest' func = function(name, 'exttest') self.eapi_positive_config_test(func, cmds) def test_update_entry(self): cmds = ['ip access-list exttest', 'no 10', '10 permit ip 0.0.0.0/32 1.1.1.1/32 log', 'exit'] func = function('update_entry', 'exttest', '10', 'permit', 'ip', '0.0.0.0', '32', '1.1.1.1', '32', True) self.eapi_positive_config_test(func, cmds) def test_update_entry_no_log(self): cmds = ['ip access-list exttest', 'no 10', '10 permit ip 0.0.0.0/32 1.1.1.1/32', 'exit'] func = function('update_entry', 'exttest', '10', 'permit', 'ip', '0.0.0.0', '32', '1.1.1.1', '32') self.eapi_positive_config_test(func, cmds) def test_remove_entry(self): cmds = ['ip access-list exttest', 'no 10', 'exit'] func = function('remove_entry', 'exttest', '10') self.eapi_positive_config_test(func, cmds) def test_add_entry(self): cmds = ['ip access-list exttest', 'permit ip 0.0.0.0/32 1.1.1.1/32 log', 'exit'] func = function('add_entry', 'exttest', 'permit', 'ip', '0.0.0.0', '32', '1.1.1.1', '32', True) self.eapi_positive_config_test(func, cmds) def test_add_entry_no_log(self): cmds = ['ip access-list exttest', 'permit ip 0.0.0.0/32 1.1.1.1/32', 'exit'] func = function('add_entry', 'exttest', 'permit', 'ip', '0.0.0.0', '32', '1.1.1.1', '32') self.eapi_positive_config_test(func, cmds) def test_add_entry_with_seqno(self): cmds = ['ip access-list exttest', '30 permit ip 0.0.0.0/32 1.1.1.1/32 log', 'exit'] func = function('add_entry', 'exttest', 'permit', 'ip', '0.0.0.0', '32', '1.1.1.1', '32', True, 30) self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_bgp.py0000644000076500000240000004465714447405743021143 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.bgp class TestApiBgp(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiBgp, self).__init__(*args, **kwargs) self.instance = pyeapi.api.bgp.instance(None) self.config = open(get_fixture('running_config.bgp')).read() def test_get(self): result = self.instance.get() keys = ['bgp_as', 'router_id', 'maximum_paths', 'maximum_ecmp_paths', 'shutdown', 'neighbors', 'networks'] self.assertEqual(sorted(keys), sorted(result.keys())) def test_create(self): for bgpas in ['65000', 65000]: func = function('create', bgpas) cmds = 'router bgp {}'.format(bgpas) self.eapi_positive_config_test(func, cmds) def test_create_invalid_as(self): for bgpas in ['66000', 66000]: with self.assertRaises(ValueError): self.instance.create(bgpas) def test_delete(self): func = function('delete') cmds = 'no router bgp 65000' self.eapi_positive_config_test(func, cmds) def test_default(self): func = function('default') cmds = 'default router bgp 65000' self.eapi_positive_config_test(func, cmds) def test_add_network(self): func = function('add_network', '172.16.10.1', '24', 'test') cmds = ['router bgp 65000', 'network 172.16.10.1/24 route-map test'] self.eapi_positive_config_test(func, cmds) func = function('add_network', '', '24', 'test') self.eapi_exception_config_test(func, ValueError) func = function('add_network', '172.16.10.1', '', 'test') self.eapi_exception_config_test(func, ValueError) def test_remove_network(self): func = function('remove_network', '172.16.10.1', '24', 'test') cmds = ['router bgp 65000', 'no network 172.16.10.1/24 route-map test'] self.eapi_positive_config_test(func, cmds) func = function('remove_network', '', '24', 'test') self.eapi_exception_config_test(func, ValueError) func = function('remove_network', '172.16.10.1', '', 'test') self.eapi_exception_config_test(func, ValueError) def test_set_router_id(self): for state in ['config', 'negate', 'default']: rid = '1.1.1.1' if state == 'config': cmds = ['router bgp 65000', 'router-id 1.1.1.1'] func = function('set_router_id', rid) elif state == 'negate': cmds = ['router bgp 65000', 'no router-id'] func = function('set_router_id', None, False, True) elif state == 'default': cmds = ['router bgp 65000', 'default router-id'] func = function('set_router_id', rid, True) self.eapi_positive_config_test(func, cmds) # func = function('set_router_id', value=None, default=False, # disable=False) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['router bgp 65000', 'no router-id'] func = function('set_router_id', None) self.eapi_positive_config_test(func, cmds) def test_maximum_paths_just_max_path(self): for state in ['config', 'negate', 'default']: max_paths = 20 if state == 'config': cmds = ['router bgp 65000', 'maximum-paths 20'] func = function('set_maximum_paths', max_paths) elif state == 'negate': cmds = ['router bgp 65000', 'no maximum-paths'] func = function('set_maximum_paths', disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default maximum-paths'] func = function('set_maximum_paths', default=True) self.eapi_positive_config_test(func, cmds) # func = function('set_maximum_paths', max_path=None, default=False, # disable=False) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['router bgp 65000', 'no maximum-paths'] func = function('set_maximum_paths', None) self.eapi_positive_config_test(func, cmds) def test_maximum_paths_max_path_and_ecmp(self): for state in ['config', 'negate', 'default']: max_paths = 20 max_ecmp_path = 20 if state == 'config': cmds = ['router bgp 65000', 'maximum-paths 20 ecmp 20'] func = function('set_maximum_paths', max_paths, max_ecmp_path) elif state == 'negate': cmds = ['router bgp 65000', 'no maximum-paths'] func = function('set_maximum_paths', disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default maximum-paths'] func = function('set_maximum_paths', default=True) self.eapi_positive_config_test(func, cmds) func = function('set_maximum_paths', max_path=None, max_ecmp_path=20, default=False, disable=False) self.eapi_exception_config_test(func, TypeError) def test_set_shutdown(self): for state in ['config', 'negate', 'default']: if state == 'config': cmds = ['router bgp 65000', 'shutdown'] func = function('set_shutdown', default=False, disable=False) elif state == 'negate': cmds = ['router bgp 65000', 'no shutdown'] func = function('set_shutdown', disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default shutdown'] func = function('set_shutdown', default=True) self.eapi_positive_config_test(func, cmds) class TestApiBgpNeighbor(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiBgpNeighbor, self).__init__(*args, **kwargs) self.instance = pyeapi.api.bgp.BgpNeighbors(None) self.config = open(get_fixture('running_config.bgp')).read() def test_getall(self): result = self.instance.getall() self.assertIsInstance(result, dict) self.assertEqual(len(result), 3) def test_get(self): result = self.instance.get('test') keys = ['name', 'send_community', 'shutdown', 'description', 'remote_as', 'next_hop_self', 'route_map_in', 'route_map_out', 'peer_group'] self.assertEqual(sorted(keys), sorted(result.keys())) def test_delete(self): cmds = ['router bgp 65000', 'no neighbor test'] func = function('delete', 'test') self.eapi_positive_config_test(func, cmds) def test_set_peer_group(self): for state in ['config', 'negate', 'default']: peer_group = 'test' name = '172.16.10.1' cmd = 'neighbor {} peer-group'.format(name) if state == 'config': cmds = ['router bgp 65000', '{} {}'.format(cmd, peer_group)] func = function('set_peer_group', name, peer_group) elif state == 'negate': cmds = ['router bgp 65000', 'no {}'.format(cmd)] func = function('set_peer_group', name, disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default {}'.format(cmd)] func = function('set_peer_group', name, peer_group, default=True) self.eapi_positive_config_test(func, cmds) # func = function('set_peer_group', name, value=None, default=False, # disable=False) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['router bgp 65000', 'no neighbor 172.16.10.1 peer-group'] func = function('set_peer_group', '172.16.10.1', None) self.eapi_positive_config_test(func, cmds) def test_set_remote_as(self): for state in ['config', 'negate', 'default']: remote_as = '65000' name = 'test' cmd = 'neighbor {} remote-as'.format(name) if state == 'config': cmds = ['router bgp 65000', '{} {}'.format(cmd, remote_as)] func = function('set_remote_as', name, remote_as) elif state == 'negate': cmds = ['router bgp 65000', 'no {}'.format(cmd)] func = function('set_remote_as', name, disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default {}'.format(cmd)] func = function('set_remote_as', name, remote_as, default=True) self.eapi_positive_config_test(func, cmds) # func = function('set_remote_as', name, value=None, default=False, # disable=False) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['router bgp 65000', 'no neighbor test remote-as'] func = function('set_remote_as', 'test', None) self.eapi_positive_config_test(func, cmds) def test_set_shutdown(self): for state in ['config', 'negate', 'default', 'false']: name = 'test' cmd = 'neighbor {}'.format(name) if state == 'config': cmds = ['router bgp 65000', '{} shutdown'.format(cmd)] func = function('set_shutdown', name, default=False, disable=False) elif state == 'negate': cmds = ['router bgp 65000', 'no {} shutdown'.format(cmd)] func = function('set_shutdown', name, disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default {} shutdown'.format(cmd)] func = function('set_shutdown', name, default=True) elif state == 'false': cmds = ['router bgp 65000', 'no {} shutdown'.format(cmd)] func = function('set_shutdown', name, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_send_community(self): for state in ['config', 'negate', 'default']: name = 'test' cmd = 'neighbor {}'.format(name) if state == 'config': cmds = ['router bgp 65000', '{} send-community'.format(cmd)] func = function('set_send_community', name, value=True) elif state == 'negate': cmds = ['router bgp 65000', 'no {} send-community'.format(cmd)] func = function('set_send_community', name, disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default {} send-community'.format(cmd)] func = function('set_send_community', name, value=False, default=True) self.eapi_positive_config_test(func, cmds) # func = function('set_send_community', name, value=None, default=False, # disable=False) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['router bgp 65000', 'no neighbor test send-community'] func = function('set_send_community', 'test', None) self.eapi_positive_config_test(func, cmds) def test_set_next_hop_self(self): for state in ['config', 'negate', 'default']: name = 'test' cmd = 'neighbor {}'.format(name) if state == 'config': cmds = ['router bgp 65000', '{} next-hop-self'.format(cmd)] func = function('set_next_hop_self', name, value=True) elif state == 'negate': cmds = ['router bgp 65000', 'no {} next-hop-self'.format(cmd)] func = function('set_next_hop_self', name, disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default {} next-hop-self'.format(cmd)] func = function('set_next_hop_self', name, value=False, default=True) self.eapi_positive_config_test(func, cmds) # func = function('set_next_hop_self', name, value=None, default=False, # disable=False) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['router bgp 65000', 'no neighbor test next-hop-self'] func = function('set_next_hop_self', 'test', None) self.eapi_positive_config_test(func, cmds) def test_set_route_map_in(self): for state in ['config', 'negate', 'default']: route_map = 'TEST_RM' name = 'test' cmd = 'neighbor {} route-map'.format(name) if state == 'config': cmds = ['router bgp 65000', '{} {} in'.format(cmd, route_map)] func = function('set_route_map_in', name, value=route_map) elif state == 'negate': cmds = ['router bgp 65000', 'no {} in'.format(cmd)] func = function('set_route_map_in', name, disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default {} in'.format(cmd)] func = function('set_route_map_in', name, value=route_map, default=True) self.eapi_positive_config_test(func, cmds) # func = function('set_route_map_in', name, value=None, default=False, # disable=False) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['router bgp 65000', 'no neighbor test route-map in'] func = function('set_route_map_in', 'test', None) self.eapi_positive_config_test(func, cmds) def test_set_route_map_out(self): for state in ['config', 'negate', 'default']: route_map = 'TEST_RM' name = 'test' cmd = 'neighbor {} route-map'.format(name) if state == 'config': cmds = ['router bgp 65000', '{} {} out'.format(cmd, route_map)] func = function('set_route_map_out', name, value=route_map) elif state == 'negate': cmds = ['router bgp 65000', 'no {} out'.format(cmd)] func = function('set_route_map_out', name, disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default {} out'.format(cmd)] func = function('set_route_map_out', name, value=route_map, default=True) self.eapi_positive_config_test(func, cmds) # func = function('set_route_map_out', name, value=None, default=False, # disable=False) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['router bgp 65000', 'no neighbor test route-map out'] func = function('set_route_map_out', 'test', None) self.eapi_positive_config_test(func, cmds) def test_set_description(self): for state in ['config', 'negate', 'default']: value = 'this is a test' name = 'test' cmd = 'neighbor {} description'.format(name) if state == 'config': cmds = ['router bgp 65000', '{} {}'.format(cmd, value)] func = function('set_description', name, value=value) elif state == 'negate': cmds = ['router bgp 65000', 'no {}'.format(cmd)] func = function('set_description', name, disable=True) elif state == 'default': cmds = ['router bgp 65000', 'default {}'.format(cmd)] func = function('set_description', name, value=value, default=True) self.eapi_positive_config_test(func, cmds) # func = function('set_description', name, value=None, default=False, # disable=False) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['router bgp 65000', 'no neighbor test description'] func = function('set_description', 'test', None) self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_interfaces.py0000644000076500000240000005554714447405743022516 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest import json sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, random_string, function, random_int from testlib import EapiConfigUnitTest import pyeapi.api.interfaces INTERFACES = ['Ethernet1', 'Ethernet1/1', 'Vlan1234', 'Management1', 'Port-Channel1', 'Vxlan1'] class TestFunctions(unittest.TestCase): def test_isvalidinterface_returns_true(self): func = pyeapi.api.interfaces.isvalidinterface for intf in INTERFACES: self.assertTrue(func(intf)) def test_isvalidinterface_returns_false(self): func = pyeapi.api.interfaces.isvalidinterface for intf in ['Et1', 'Ma1', 'Po1', 'Vl1', random_string()]: self.assertFalse(func(intf)) def test_instance(self): result = pyeapi.api.interfaces.instance(None) self.assertIsInstance(result, pyeapi.api.interfaces.Interfaces) class TestApiInterfaces(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiInterfaces, self).__init__(*args, **kwargs) self.instance = pyeapi.api.interfaces.Interfaces(None) self.config = open(get_fixture('running_config.text')).read() def test_get_interface_generic(self): for intf in ['Management1', 'Loopback0']: result = self.instance.get(intf) self.assertEqual(result['type'], 'generic') def test_get_interface_ethernet(self): result = self.instance.get('Ethernet1') self.assertEqual(result['type'], 'ethernet') def test_get_invalid_interface(self): result = self.instance.get('Foo1') self.assertEqual(result, None) def test_proxy_method_success(self): result = self.instance.set_sflow('Ethernet1', True) self.assertTrue(result) def test_proxy_method_raises_attribute_error(self): with self.assertRaises(AttributeError): self.instance.set_sflow('Management1', True) class TestApiBaseInterface(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiBaseInterface, self).__init__(*args, **kwargs) self.instance = pyeapi.api.interfaces.BaseInterface(None) self.config = open(get_fixture('running_config.text')).read() def test_get(self): result = self.instance.get('Loopback0') values = dict(name='Loopback0', type='generic', shutdown=False, description=None) self.assertEqual(result, values) def test_set_description_with_value(self): for intf in INTERFACES: value = random_string() cmds = ['interface %s' % intf, 'description %s' % value] func = function('set_description', intf, value) self.eapi_positive_config_test(func, cmds) def test_set_description_with_no_value(self): for intf in INTERFACES: cmds = ['interface %s' % intf, 'no description'] func = function('set_description', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_description_with_default(self): for intf in INTERFACES: cmds = ['interface %s' % intf, 'default description'] func = function('set_description', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_shutdown(self): for intf in INTERFACES: cmds = ['interface %s' % intf, 'shutdown'] func = function('set_shutdown', intf, default=False, disable=False) self.eapi_positive_config_test(func, cmds) def test_set_shutdown_with_disable(self): for intf in INTERFACES: cmds = ['interface %s' % intf, 'no shutdown'] func = function('set_shutdown', intf) self.eapi_positive_config_test(func, cmds) def test_set_shutdown_with_default(self): for intf in INTERFACES: cmds = ['interface %s' % intf, 'default shutdown'] func = function('set_shutdown', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_encapsulation_non_subintf(self): cmds = ['interface Ethernet1', 'encapsulation dot1q vlan 4'] func = function('set_encapsulation', 'Ethernet1', 4) self.eapi_exception_config_test(func, NotImplementedError, cmds) def test_set_encapsulation_non_supported_intf(self): cmds = ['interface Vlan1234', 'encapsulation dot1q vlan 4'] func = function('set_encapsulation', 'Vlan1234', 4) self.eapi_exception_config_test(func, NotImplementedError, cmds) def test_set_encapsulation_ethernet_subintf(self): cmds = ['interface Ethernet1.1', 'encapsulation dot1q vlan 4'] func = function('set_encapsulation', 'Ethernet1.1', 4) self.eapi_positive_config_test(func, cmds) def test_set_encapsulation_portchannel_subintf_disable(self): cmds = ['interface Port-Channel1.1', 'no encapsulation dot1q vlan'] func = function('set_encapsulation', 'Port-Channel1.1', 4, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_encapsulation_ethernet_subintf_default(self): cmds = ['interface Ethernet1.1', 'default encapsulation dot1q vlan'] func = function('set_encapsulation', 'Ethernet1.1', 4, default=True) self.eapi_positive_config_test(func, cmds) class TestApiEthernetInterface(EapiConfigUnitTest): INTERFACES = ['Ethernet1', 'Ethernet1/1'] def __init__(self, *args, **kwargs): super(TestApiEthernetInterface, self).__init__(*args, **kwargs) self.instance = pyeapi.api.interfaces.EthernetInterface(None) self.config = open(get_fixture('running_config.text')).read() def test_get(self): result = self.instance.get('Ethernet1') values = dict(name='Ethernet1', type='ethernet', description=None, shutdown=False, sflow=True, flowcontrol_send='off', flowcontrol_receive='off') self.assertEqual(values, result) def test_instance_functions(self): for intf in self.INTERFACES: for name in ['create', 'delete', 'default']: if name == 'create': # Test create for subinterfaces subintf = intf + '.1' cmds = ['interface %s' % subintf] func = function(name, subintf) self.eapi_positive_config_test(func, cmds) elif name == 'delete': # Test delete for subinterfaces subintf = intf + '.1' cmds = ['no interface %s' % subintf] func = function(name, subintf) self.eapi_positive_config_test(func, cmds) elif name == 'default': cmds = 'default interface %s' % intf func = function(name, intf) self.eapi_positive_config_test(func, cmds) def test_instance_functions_exceptions(self): intf = 'Ethernet1' for name in ['create', 'delete']: if name == 'create': cmds = 'interface %s' % intf func = function(name, intf) self.eapi_exception_config_test(func, NotImplementedError, cmds) elif name == 'delete': cmds = 'no interface %s' % intf func = function(name, intf) self.eapi_exception_config_test(func, NotImplementedError, cmds) def test_set_flowcontrol_with_value(self): for intf in self.INTERFACES: for direction in ['send', 'receive']: for value in ['on', 'off']: cmds = ['interface %s' % intf, 'flowcontrol %s %s' % (direction, value)] func = function('set_flowcontrol', intf, direction, value) self.eapi_positive_config_test(func, cmds) def test_set_flowcontrol_with_invalid_direction_raises_value_error(self): for intf in self.INTERFACES: func = function('set_flowcontrol', intf, 'invalid', None) self.eapi_exception_config_test(func, ValueError) def test_set_flowcontrol_with_invalid_value_raises_value_error(self): for intf in self.INTERFACES: for direction in ['send', 'receive']: func = function('set_flowcontrol', intf, direction, 'invalid') self.eapi_exception_config_test(func, ValueError) def test_set_flowcontrol_with_no_value(self): for intf in self.INTERFACES: for direction in ['send', 'receive']: cmds = ['interface %s' % intf, 'no flowcontrol %s' % direction] func = function('set_flowcontrol', intf, direction) self.eapi_positive_config_test(func, cmds) def test_set_flowcontrol_with_disable(self): for intf in self.INTERFACES: for direction in ['send', 'receive']: cmds = ['interface %s' % intf, 'no flowcontrol %s' % direction] func = function('set_flowcontrol', intf, direction, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_flowcontrol_with_default(self): for intf in self.INTERFACES: for direction in ['send', 'receive']: cmds = ['interface %s' % intf, 'default flowcontrol %s' % direction] func = function('set_flowcontrol', intf, direction, default=True) self.eapi_positive_config_test(func, cmds) def test_set_sflow_with_value(self): for intf in self.INTERFACES: for value in [True, False]: cmds = ['interface %s' % intf] if value: cmds.append('sflow enable') else: cmds.append('no sflow enable') func = function('set_sflow', intf, value) self.eapi_positive_config_test(func, cmds) def test_set_sflow_with_no_value(self): for intf in INTERFACES: cmds = ['interface %s' % intf, 'no sflow enable'] func = function('set_sflow', intf) self.eapi_positive_config_test(func, cmds) def test_set_sflow_with_default(self): for intf in INTERFACES: cmds = ['interface %s' % intf, 'default sflow enable'] func = function('set_sflow', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_sflow_invalid_value_raises_value_error(self): for intf in INTERFACES: func = function('set_sflow', intf, random_string()) self.eapi_exception_config_test(func, ValueError) def test_set_vrf(self): for intf in INTERFACES: vrf = 'testvrf' cmds = ['interface %s' % intf, 'vrf forwarding %s' % vrf] func = function('set_vrf', intf, vrf) self.eapi_positive_config_test(func, cmds) class TestApiPortchannelInterface(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiPortchannelInterface, self).__init__(*args, **kwargs) self.instance = pyeapi.api.interfaces.PortchannelInterface(None) self.config = open(get_fixture('running_config.portchannel')).read() def setUp(self): super(TestApiPortchannelInterface, self).setUp() response = open(get_fixture('show_portchannel.json')) self.node.enable.return_value = json.load(response) response.close() def test_get(self): result = self.instance.get('Port-Channel1') values = dict(name='Port-Channel1', type='portchannel', description=None, shutdown=False, lacp_mode='on', minimum_links=0, lacp_fallback='disabled', lacp_timeout=90, members=['Ethernet5', 'Ethernet6']) self.assertEqual(values, result) def test_set_minimum_links_with_value(self): minlinks = random_int(1, 16) cmds = ['interface Port-Channel1', 'port-channel min-links %s' % minlinks] func = function('set_minimum_links', 'Port-Channel1', minlinks) self.eapi_positive_config_test(func, cmds) def test_set_minimum_links_with_no_value(self): cmds = ['interface Port-Channel1', 'no port-channel min-links'] func = function('set_minimum_links', 'Port-Channel1') self.eapi_positive_config_test(func, cmds) def test_set_minimum_links_with_default(self): cmds = ['interface Port-Channel1', 'default port-channel min-links'] func = function('set_minimum_links', 'Port-Channel1', default=True) self.eapi_positive_config_test(func, cmds) def test_set_minimum_links_with_disable(self): cmds = ['interface Port-Channel1', 'no port-channel min-links'] func = function('set_minimum_links', 'Port-Channel1', disable=True) self.eapi_positive_config_test(func, cmds) def test_set_lacp_timeout_with_value(self): timeout = random_int(1, 16) cmds = ['interface Port-Channel1', 'port-channel lacp fallback timeout %s' % timeout] func = function('set_lacp_timeout', 'Port-Channel1', timeout) self.eapi_positive_config_test(func, cmds) def test_set_lacp_fallback_with_individual(self): cmds = ['interface Port-Channel1', 'port-channel lacp fallback individual'] func = function('set_lacp_fallback', 'Port-Channel1', 'individual') self.eapi_positive_config_test(func, cmds) def test_set_lacp_fallback_with_static(self): cmds = ['interface Port-Channel1', 'port-channel lacp fallback static'] func = function('set_lacp_fallback', 'Port-Channel1', 'static') self.eapi_positive_config_test(func, cmds) def test_set_lacp_fallback_with_disabled(self): cmds = ['interface Port-Channel1', 'no port-channel lacp fallback'] func = function('set_lacp_fallback', 'Port-Channel1', 'disabled') self.eapi_positive_config_test(func, cmds) def test_set_lacp_fallback_invalid_mode(self): func = function('set_lacp_fallback', 'Port-Channel1', random_string()) self.eapi_negative_config_test(func) def test_get_lacp_mode(self): result = self.instance.get_lacp_mode('Port-Channel1') self.assertEqual(result, 'on') def test_get_members(self): result = self.instance.get_members('Port-Channel1') self.assertEqual(result, ['Ethernet5', 'Ethernet6']) def test_set_members(self): cmds = ['interface Ethernet6', 'no channel-group 1', 'interface Ethernet7', 'channel-group 1 mode on'] func = function('set_members', 'Port-Channel1', ['Ethernet5', 'Ethernet7']) self.eapi_positive_config_test(func, cmds) def test_set_members_same_mode(self): cmds = ['interface Ethernet6', 'no channel-group 1', 'interface Ethernet7', 'channel-group 1 mode on'] func = function('set_members', 'Port-Channel1', ['Ethernet5', 'Ethernet7']) self.eapi_positive_config_test(func, cmds) def test_set_members_update_mode(self): cmds = ['interface Ethernet6', 'no channel-group 1', 'interface Ethernet7', 'channel-group 1 mode active'] func = function('set_members', 'Port-Channel1', ['Ethernet5', 'Ethernet7'], mode='active') self.eapi_positive_config_test(func, cmds) def test_set_members_mode_none(self): cmds = ['interface Ethernet6', 'no channel-group 1', 'interface Ethernet7', 'channel-group 1 mode on'] func = function('set_members', 'Port-Channel1', ['Ethernet5', 'Ethernet7'], mode=None) self.eapi_positive_config_test(func, cmds) def test_set_members_no_changes(self): func = function('set_members', 'Port-Channel1', ['Ethernet5', 'Ethernet6']) self.eapi_positive_config_test(func) def test_set_lacp_mode(self): cmds = ['interface Ethernet5', 'no channel-group 1', 'interface Ethernet6', 'no channel-group 1', 'interface Ethernet5', 'channel-group 1 mode active', 'interface Ethernet6', 'channel-group 1 mode active'] func = function('set_lacp_mode', 'Port-Channel1', 'active') self.eapi_positive_config_test(func, cmds) def test_set_lacp_mode_invalid_mode(self): func = function('set_lacp_mode', 'Port-Channel1', random_string()) self.eapi_negative_config_test(func) class TestApiVxlanInterface(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiVxlanInterface, self).__init__(*args, **kwargs) self.instance = pyeapi.api.interfaces.VxlanInterface(None) self.config = open(get_fixture('running_config.vxlan')).read() def test_get(self): keys = ['name', 'type', 'description', 'shutdown', 'source_interface', 'multicast_group', 'udp_port', 'vlans', 'flood_list', 'multicast_decap'] result = self.instance.get('Vxlan1') self.assertEqual(sorted(keys), sorted(result.keys())) def test_set_source_interface_with_value(self): cmds = ['interface Vxlan1', 'vxlan source-interface Loopback0'] func = function('set_source_interface', 'Vxlan1', 'Loopback0') self.eapi_positive_config_test(func, cmds) def test_set_source_interface_with_no_value(self): cmds = ['interface Vxlan1', 'no vxlan source-interface'] func = function('set_source_interface', 'Vxlan1', disable=True) self.eapi_positive_config_test(func, cmds) def test_set_source_interface_with_default(self): cmds = ['interface Vxlan1', 'default vxlan source-interface'] func = function('set_source_interface', 'Vxlan1', default=True) self.eapi_positive_config_test(func, cmds) def test_set_multicast_group_with_value(self): cmds = ['interface Vxlan1', 'vxlan multicast-group 239.10.10.10'] func = function('set_multicast_group', 'Vxlan1', '239.10.10.10') self.eapi_positive_config_test(func, cmds) def test_set_multicast_group_with_no_value(self): cmds = ['interface Vxlan1', 'no vxlan multicast-group'] func = function('set_multicast_group', 'Vxlan1', disable=True) self.eapi_positive_config_test(func, cmds) def test_set_multicast_group_with_default(self): cmds = ['interface Vxlan1', 'default vxlan multicast-group'] func = function('set_multicast_group', 'Vxlan1', default=True) self.eapi_positive_config_test(func, cmds) def test_set_multicast_decap(self): cmds = ['interface Vxlan1', 'vxlan multicast-group decap'] func = function('set_multicast_decap', 'Vxlan1') self.eapi_positive_config_test(func, cmds) def test_set_multicast_decap_with_no_value(self): cmds = ['interface Vxlan1', 'no vxlan multicast-group decap'] func = function('set_multicast_decap', 'Vxlan1', disable=True) self.eapi_positive_config_test(func, cmds) def test_set_multicast_decap_with_default(self): cmds = ['interface Vxlan1', 'default vxlan multicast-group decap'] func = function('set_multicast_decap', 'Vxlan1', default=True) self.eapi_positive_config_test(func, cmds) def test_set_udp_port_with_value(self): cmds = ['interface Vxlan1', 'vxlan udp-port 1024'] func = function('set_udp_port', 'Vxlan1', '1024') self.eapi_positive_config_test(func, cmds) def test_set_udp_port_with_no_value(self): cmds = ['interface Vxlan1', 'no vxlan udp-port'] func = function('set_udp_port', 'Vxlan1', disable=True) self.eapi_positive_config_test(func, cmds) def test_set_udp_port_with_default(self): cmds = ['interface Vxlan1', 'default vxlan udp-port'] func = function('set_udp_port', 'Vxlan1', default=True) self.eapi_positive_config_test(func, cmds) def test_update_vlan(self): cmds = ['interface Vxlan1', 'vxlan vlan add 10 vni 10'] func = function('update_vlan', 'Vxlan1', 10, 10) self.eapi_positive_config_test(func, cmds) def test_remove_vlan(self): cmds = ['interface Vxlan1', 'vxlan vlan remove 10 vni'] func = function('remove_vlan', 'Vxlan1', 10) self.eapi_positive_config_test(func, cmds) def test_add_vtep(self): cmds = ['interface Vxlan1', 'vxlan flood vtep add 1.1.1.1'] func = function('add_vtep', 'Vxlan1', '1.1.1.1') self.eapi_positive_config_test(func, cmds) def test_add_vtep_to_vlan(self): cmds = ['interface Vxlan1', 'vxlan vlan 10 flood vtep add 1.1.1.1'] func = function('add_vtep', 'Vxlan1', '1.1.1.1', vlan='10') self.eapi_positive_config_test(func, cmds) def test_remove_vtep(self): cmds = ['interface Vxlan1', 'vxlan flood vtep remove 1.1.1.1'] func = function('remove_vtep', 'Vxlan1', '1.1.1.1') self.eapi_positive_config_test(func, cmds) def test_remove_vtep_from_vlan(self): cmds = ['interface Vxlan1', 'vxlan vlan 10 flood vtep remove 1.1.1.1'] func = function('remove_vtep', 'Vxlan1', '1.1.1.1', vlan='10') self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_ipinterfaces.py0000644000076500000240000001377614447405743023045 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import os import unittest import sys sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function, random_int, random_string from testlib import EapiConfigUnitTest import pyeapi.api.ipinterfaces class TestApiIpinterfaces(EapiConfigUnitTest): INTERFACES = ['Ethernet1', 'Ethernet1/1', 'Vlan1234', 'Management1', 'Port-Channel1'] def __init__(self, *args, **kwargs): super(TestApiIpinterfaces, self).__init__(*args, **kwargs) self.instance = pyeapi.api.ipinterfaces.instance(None) self.config = open(get_fixture('running_config.text')).read() def test_get(self): result = self.instance.get( 'Loopback0' ) values = dict( name='Loopback0', address='1.1.1.1/32', mtu=1500 ) self.assertEqual( result, values ) # test interface with secondary ip result = self.instance.get( 'Loopback2' ) values = dict( name='Loopback2', address='2.2.2.2/32', secondary=['3.255.255.1/24', '4.255.255.1/24'], mtu=None ) self.assertEqual(result, values) def test_getall(self): result = self.instance.getall() self.assertIsInstance(result, dict) self.assertEqual(len(result), 4) def test_instance_functions(self): for intf in self.INTERFACES: for name in ['create', 'delete']: if name == 'create': cmds = ['interface %s' % intf, 'no switchport'] elif name == 'delete': cmds = ['interface %s' % intf, 'no ip address', 'switchport'] func = function(name, intf) self.eapi_positive_config_test(func, cmds) def test_set_address_with_value(self): for intf in self.INTERFACES: value = '1.2.3.4/5' cmds = ['interface %s' % intf, 'ip address 1.2.3.4/5'] func = function('set_address', intf, value) self.eapi_positive_config_test(func, cmds) def test_set_address_with_no_value(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no ip address'] func = function('set_address', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_address_with_default(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'default ip address'] func = function('set_address', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_address_invalid_value_raises_value_error(self): for intf in self.INTERFACES: # func = function('set_address', intf, None) # self.eapi_exception_config_test(func, ValueError) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['interface %s' % intf, 'no ip address'] func = function('set_address', intf, value=None) self.eapi_positive_config_test(func, cmds) def test_set_mtu_with_values(self): for intf in self.INTERFACES: for value in [68, 65535, random_int(68, 65535)]: cmds = ['interface %s' % intf, 'mtu %s' % value] func = function('set_mtu', intf, value) self.eapi_positive_config_test(func, cmds) def test_set_mtu_with_no_value(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no mtu'] func = function('set_mtu', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_mtu_default(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'default mtu'] func = function('set_mtu', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_mtu_invalid_value_raises_value_error(self): for intf in self.INTERFACES: for value in [67, 65536, 'a' + random_string()]: func = function('set_mtu', intf, value) self.eapi_exception_config_test(func, ValueError) for value in [None]: # If command_builder fails because value is None, put None # in the first loop to check for value error, and remove # this second loop cmds = ['interface %s' % intf, 'no mtu'] func = function('set_mtu', intf, value) self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_mlag.py0000644000076500000240000001475014447405743021302 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.mlag class TestApiMlag(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiMlag, self).__init__(*args, **kwargs) self.instance = pyeapi.api.mlag.instance(None) self.config = open(get_fixture('running_config.text')).read() def test_get(self): result = self.instance.get() keys = ['config', 'interfaces'] intfkeys = ['mlag_id'] interfaces = result['interfaces']['Port-Channel10'] cfgkeys = ['domain_id', 'local_interface', 'peer_address', 'peer_link', 'shutdown'] self.assertEqual(sorted(keys), sorted(result.keys())) self.assertEqual(sorted(cfgkeys), sorted(result['config'].keys())) self.assertEqual(sorted(intfkeys), sorted(interfaces.keys())) def test_set_domain_id(self): for state in ['config', 'negate', 'default']: cmds = ['mlag configuration'] if state == 'config': cmds.append('domain-id test.dom-id string') func = function('set_domain_id', 'test.dom-id string') elif state == 'negate': cmds.append('no domain-id') func = function('set_domain_id', value='test', disable=True) elif state == 'default': cmds.append('default domain-id') func = function('set_domain_id', value='test', default=True) self.eapi_positive_config_test(func, cmds) def test_set_local_interface(self): for state in ['config', 'negate', 'default']: cmds = ['mlag configuration'] if state == 'config': cmds.append('local-interface Vlan1234') func = function('set_local_interface', 'Vlan1234') elif state == 'negate': cmds.append('no local-interface') func = function('set_local_interface', disable=True) elif state == 'default': cmds.append('default local-interface') func = function('set_local_interface', value='Vlan1234', default=True) self.eapi_positive_config_test(func, cmds) def test_set_peer_address(self): for state in ['config', 'negate', 'default']: cmds = ['mlag configuration'] if state == 'config': cmds.append('peer-address 1.2.3.4') func = function('set_peer_address', '1.2.3.4') elif state == 'negate': cmds.append('no peer-address') func = function('set_peer_address', disable=True) elif state == 'default': cmds.append('default peer-address') func = function('set_peer_address', value='1.2.3.4', default=True) self.eapi_positive_config_test(func, cmds) def test_set_peer_link(self): for state in ['config', 'negate', 'default']: cmds = ['mlag configuration'] if state == 'config': cmds.append('peer-link Ethernet1') func = function('set_peer_link', 'Ethernet1') elif state == 'negate': cmds.append('no peer-link') func = function('set_peer_link', disable=True) elif state == 'default': cmds.append('default peer-link') func = function('set_peer_link', value='Ethernet1', default=True) self.eapi_positive_config_test(func, cmds) def test_set_shutdown(self): for state in ['config', 'negate', 'default']: cmds = ['mlag configuration'] if state == 'config': cmds.append('shutdown') func = function('set_shutdown', default=False, disable=False) elif state == 'negate': cmds.append('no shutdown') func = function('set_shutdown', disable=True) elif state == 'default': cmds.append('default shutdown') func = function('set_shutdown', default=True) self.eapi_positive_config_test(func, cmds) def test_set_mlag_id(self): for state in ['config', 'negate', 'default']: if state == 'config': cmds = ['interface Ethernet1', 'mlag 1'] func = function('set_mlag_id', 'Ethernet1', '1') elif state == 'negate': cmds = ['interface Ethernet1', 'no mlag'] func = function('set_mlag_id', 'Ethernet1', disable=True) elif state == 'default': cmds = ['interface Ethernet1', 'default mlag'] func = function('set_mlag_id', 'Ethernet1', value='1', default=True) self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_ntp.py0000644000076500000240000001051114447405743021152 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.ntp class TestApiNtp(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiNtp, self).__init__(*args, **kwargs) self.instance = pyeapi.api.ntp.Ntp(None) self.config = open(get_fixture('running_config.text')).read() def test_instance(self): result = pyeapi.api.ntp.instance(None) self.assertIsInstance(result, pyeapi.api.ntp.Ntp) def test_get(self): result = self.instance.get() ntp = {'servers': [{'1.2.3.4': 'prefer'}, {'10.20.30.40': None}, {'11.22.33.44': None}, {'123.33.22.11': 'prefer'}, {'123.44.55.66': None}, {'joe': None}], 'source_interface': 'Loopback1'} keys = ['source_interface', 'servers'] self.assertEqual(sorted(keys), sorted(result.keys())) self.assertEqual(ntp['source_interface'], result['source_interface']) self.assertIsNotNone(result['servers']) def test_create(self): cmd = 'ntp source Ethernet2' func = function('create', 'Ethernet2') self.eapi_positive_config_test(func, cmd) def test_delete(self): cmd = 'no ntp source' func = function('delete') self.eapi_positive_config_test(func, cmd) def test_default(self): cmd = 'default ntp source' func = function('default') self.eapi_positive_config_test(func, cmd) def test_set_source_interface(self): cmd = 'ntp source Vlan50' func = function('set_source_interface', 'Vlan50') self.eapi_positive_config_test(func, cmd) def test_add_server(self): cmd = 'ntp server 1.1.1.1' func = function('add_server', '1.1.1.1') self.eapi_positive_config_test(func, cmd) def test_add_server_prefer(self): cmd = 'ntp server 1.1.1.1 prefer' func = function('add_server', '1.1.1.1', prefer=True) self.eapi_positive_config_test(func, cmd) def test_add_server_invalid(self): func = function('add_server', '', prefer=True) self.eapi_exception_config_test(func, ValueError) func = function('add_server', ' ', prefer=True) self.eapi_exception_config_test(func, ValueError) def test_remove_server(self): cmd = 'no ntp server 1.1.1.1' func = function('remove_server', '1.1.1.1') self.eapi_positive_config_test(func, cmd) def test_remove_all_servers(self): cmd = 'no ntp' func = function('remove_all_servers') self.eapi_positive_config_test(func, cmd) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_ospf.py0000644000076500000240000001346514447405743021333 0ustar dlyssenkostaff00000000000000import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.ospf class TestApiOspf(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiOspf, self).__init__(*args, **kwargs) self.instance = pyeapi.api.ospf.instance(None) self.config = open(get_fixture('running_config.ospf')).read() def test_get_no_vrf(self): result = self.instance.get() keys = ['networks', 'ospf_process_id', 'vrf', 'redistributions', 'router_id', 'shutdown'] self.assertEqual(sorted(keys), sorted(result.keys())) self.assertEqual(result['vrf'], 'default') def test_get_with_vrf(self): result = self.instance.get(vrf='test') keys = ['networks', 'ospf_process_id', 'vrf', 'redistributions', 'router_id', 'shutdown'] self.assertEqual(sorted(keys), sorted(result.keys())) self.assertEqual(result['vrf'], 'test') def test_create(self): for ospf_id in ['65000', 65000]: func = function('create', ospf_id) cmds = 'router ospf {}'.format(ospf_id) self.eapi_positive_config_test(func, cmds) def test_create_with_vrf(self): for ospf_id in ['65000', 65000]: vrf_name = 'test' func = function('create', ospf_id, vrf_name) cmds = 'router ospf {} vrf {}'.format(ospf_id, vrf_name) self.eapi_positive_config_test(func, cmds) def test_create_invalid_id(self): for ospf_id in ['66000', 66000]: with self.assertRaises(ValueError): self.instance.create(ospf_id) def test_delete(self): func = function('delete') cmds = 'no router ospf 65000' self.eapi_positive_config_test(func, cmds) def test_add_network(self): func = function('add_network', '172.16.10.0', '24', '0') cmds = ['router ospf 65000', 'network 172.16.10.0/24 area 0'] self.eapi_positive_config_test(func, cmds) func = function('add_network', '', '24', '0') self.eapi_exception_config_test(func, ValueError) func = function('add_network', '172.16.10.0', '', '0') self.eapi_exception_config_test(func, ValueError) def test_remove_network(self): func = function('remove_network', '172.16.10.0', '24', '0') cmds = ['router ospf 65000', 'no network 172.16.10.0/24 area 0'] self.eapi_positive_config_test(func, cmds) func = function('remove_network', '', '24', '0') self.eapi_exception_config_test(func, ValueError) func = function('remove_network', '172.16.10.0', '', '0') self.eapi_exception_config_test(func, ValueError) def test_set_router_id(self): for state in ['config', 'negate', 'default']: rid = '1.1.1.1' if state == 'config': cmds = ['router ospf 65000', 'router-id 1.1.1.1'] func = function('set_router_id', rid) elif state == 'negate': cmds = ['router ospf 65000', 'no router-id'] func = function('set_router_id') elif state == 'default': cmds = ['router ospf 65000', 'default router-id'] func = function('set_router_id', rid, True) self.eapi_positive_config_test(func, cmds) cmds = ['router ospf 65000', 'no router-id'] func = function('set_router_id') self.eapi_positive_config_test(func, cmds) def test_set_shutdown(self): for state in ['config', 'negate', 'default']: if state == 'config': cmds = ['router ospf 65000', 'shutdown'] func = function('set_shutdown') elif state == 'negate': cmds = ['router ospf 65000', 'no shutdown'] func = function('set_no_shutdown') self.eapi_positive_config_test(func, cmds) def test_add_redistribution_no_route_map(self): for protocol in ['bgp', 'rip', 'static', 'connected', 'no-proto']: cmds = ['router ospf 65000', 'redistribute {}'.format(protocol)] func = function('add_redistribution', protocol) if protocol != 'no-proto': self.eapi_positive_config_test(func, cmds) else: self.eapi_exception_config_test(func, ValueError) def test_add_redistribution_with_route_map(self): for protocol in ['bgp', 'rip', 'static', 'connected']: cmds = ['router ospf 65000', 'redistribute {} route-map test'.format(protocol)] func = function('add_redistribution', protocol, 'test') if protocol != 'no-proto': self.eapi_positive_config_test(func, cmds) else: self.eapi_exception_config_test(func, ValueError) def test_delete_redistribution_no_route_map(self): for protocol in ['bgp', 'rip', 'static', 'connected', 'no-proto']: cmds = ['router ospf 65000', 'no redistribute {}'.format(protocol)] func = function('remove_redistribution', protocol) if protocol != 'no-proto': self.eapi_positive_config_test(func, cmds) else: self.eapi_exception_config_test(func, ValueError) class TestApiNegOspf(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiNegOspf, self).__init__(*args, **kwargs) self.instance = pyeapi.api.ospf.instance(None) self.config = open(get_fixture('running_config.bgp')).read() def test_no_get(self): result = self.instance.get() self.assertEqual(None, result) def test_no_delete(self): result = self.instance.delete() self.assertTrue(result) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_routemaps.py0000644000076500000240000001555114447405743022401 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function, random_string from testlib import EapiConfigUnitTest import pyeapi.api.routemaps class TestApiRoutemaps(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiRoutemaps, self).__init__(*args, **kwargs) self.instance = pyeapi.api.routemaps.Routemaps(None) self.config = open(get_fixture('running_config.routemaps')).read() def test_instance(self): result = pyeapi.api.routemaps.instance(None) self.assertIsInstance(result, pyeapi.api.routemaps.Routemaps) def test_get(self): result = self.instance.get('TEST') keys = ['deny', 'permit'] self.assertEqual(sorted(keys), sorted(result.keys())) def test_get_not_configured(self): self.assertIsNone(self.instance.get('blah')) def test_getall(self): # Review fixtures/running_config.routemaps to see the default # running-config that is the basis for this test result = self.instance.getall() self.assertIsInstance(result, dict) self.assertEqual(len(result.keys()), 4) def test_routemaps_functions(self): for name in ['create', 'delete', 'default']: if name == 'create': cmds = 'route-map new permit 100' elif name == 'delete': cmds = 'no route-map new permit 100' elif name == 'default': cmds = 'default route-map new permit 100' func = function(name, 'new', 'permit', 100) self.eapi_positive_config_test(func, cmds) def test_set_set_statement_clean(self): cmds = ['route-map new permit 100', 'set weight 100'] func = function('set_set_statements', 'new', 'permit', 100, ['weight 100']) self.eapi_positive_config_test(func, cmds) def test_set_set_statement_remove_extraneous(self): # Review fixtures/running_config.routemaps to see the default # running-config that is the basis for this test cmds = ['route-map TEST permit 10', 'no set tag 50', 'route-map TEST permit 10', 'set weight 100'] func = function('set_set_statements', 'TEST', 'permit', 10, ['weight 100']) self.eapi_positive_config_test(func, cmds) def test_set_match_statement_clean(self): cmds = ['route-map new permit 200', 'match as 100'] func = function('set_match_statements', 'new', 'permit', 200, ['as 100']) self.eapi_positive_config_test(func, cmds) def test_set_match_statement_remove_extraneous(self): # Review fixtures/running_config.routemaps to see the default # running-config that is the basis for this test cmds = ['route-map TEST permit 10', 'no match interface Ethernet1', 'route-map TEST permit 10', 'match as 1000'] func = function('set_match_statements', 'TEST', 'permit', 10, ['as 1000']) self.eapi_positive_config_test(func, cmds) def test_set_continue(self): cmds = ['route-map TEST permit 10', 'continue 100'] func = function('set_continue', 'TEST', 'permit', 10, 100) self.eapi_positive_config_test(func, cmds) def test_set_continue_with_invalid_integer(self): with self.assertRaises(ValueError): self.instance.set_continue('TEST', 'permit', 10, -1) def test_set_continue_with_invalid_string(self): with self.assertRaises(ValueError): self.instance.set_continue('TEST', 'permit', 10, 'invalid') def test_set_continue_to_default(self): cmds = ['route-map TEST permit 10', 'default continue'] func = function('set_continue', 'TEST', 'permit', 10, default=True) self.eapi_positive_config_test(func, cmds) def test_negate_continue(self): cmds = ['route-map TEST permit 10', 'no continue'] func = function('set_continue', 'TEST', 'permit', 10, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_description_with_value(self): value = random_string() cmds = ['route-map TEST permit 10', 'no description', 'description %s' % value] func = function('set_description', 'TEST', 'permit', 10, value) self.eapi_positive_config_test(func, cmds) def test_negate_description(self): cmds = ['route-map TEST permit 10', 'no description'] func = function('set_description', 'TEST', 'permit', 10, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_description_with_default(self): cmds = ['route-map TEST permit 10', 'default description'] func = function('set_description', 'TEST', 'permit', 10, default=True) self.eapi_positive_config_test(func, cmds) def test_set_description_with_invalid_value(self): # with self.assertRaises(ValueError): # self.instance.set_description('TEST', 'permit', 10, value=None) # If command_builder fails because value is None, uncomment # above lines and remove below lines. cmds = ['route-map TEST permit 10', 'no description'] func = function('set_description', 'TEST', 'permit', 10, value=None) self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_staticroute.py0000644000076500000240000002646414447405743022735 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from random import choice from testlib import get_fixture, function, random_int, random_string from testlib import EapiConfigUnitTest import pyeapi.api.staticroute IP_DESTS = ['11.111.11.0/24', '222.22.222.0/24', '33.34.35.0/24'] NEXT_HOPS = [('Ethernet1', '3.3.3.3'), ('Ethernet2', '2.2.2.2'), ('Null0', None), ('44.44.44.0', None)] DISTANCES = TAGS = ROUTE_NAMES = [None, True] class TestApiStaticroute(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiStaticroute, self).__init__(*args, **kwargs) self.instance = pyeapi.api.staticroute.StaticRoute(None) self.config = open(get_fixture('running_config.text')).read() def test_instance(self): result = pyeapi.api.staticroute.instance(None) self.assertIsInstance(result, pyeapi.api.staticroute.StaticRoute) def test_get(self): # Test retrieval of a specific static route entry # Assumes running_config.text file contains the following # ip route specifications, and that no additional routes # are specified. # ip route 0.0.0.0/0 192.68.1.254 1 tag 0 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 # Get the route(s) for ip_dest 0.0.0.0/24 ip_dest = '0.0.0.0/0' routes = { '192.68.1.254': { None: { 1: {'route_name': None, 'tag': 0} } } } result = self.instance.get(ip_dest) self.assertEqual(result, routes) # Get the route(s) for ip_dest 1.2.3.0/24 ip_dest = '1.2.3.0/24' routes = { 'Ethernet1': { '1.1.1.1': { 1: { 'route_name': 'test1', 'tag': 1}, 10: { 'route_name': 'test1', 'tag': 1} }, '10.1.1.1': { 20: { 'route_name': 'test1', 'tag': 1} } } } result = self.instance.get(ip_dest) self.assertEqual(result, routes) def test_getall(self): # Test retrieval of all static route entries # Assumes running_config.text file contains the following # ip route specifications, and that no additional routes # are specified. # ip route 0.0.0.0/0 192.68.1.254 1 tag 0 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 1 tag 1 name test1 # ip route 1.2.3.0/24 Ethernet1 1.1.1.1 10 tag 1 name test1 # ip route 1.2.3.0/24 Ethernet1 10.1.1.1 20 tag 1 name test1 routes = { '0.0.0.0/0': { '192.68.1.254': { None: { 1: {'route_name': None, 'tag': 0} } } }, '1.2.3.0/24': { 'Ethernet1': { '1.1.1.1': { 1: { 'route_name': 'test1', 'tag': 1}, 10: { 'route_name': 'test1', 'tag': 1} }, '10.1.1.1': { 20: { 'route_name': 'test1', 'tag': 1} } } } } self.maxDiff = None result = self.instance.getall() self.assertEqual(result, routes) def test_create(self): # Test passing in a full set of parameters to 'create' # Some parameters may be not set: None for ip_dest in IP_DESTS: # Get the parameters for the call (next_hop, next_hop_ip) = choice(NEXT_HOPS) distance = choice(DISTANCES) if distance: distance = random_int(0, 255) tag = choice(TAGS) if tag: tag = random_int(0, 255) route_name = choice(ROUTE_NAMES) if route_name: route_name = random_string(minchar=4, maxchar=10) func = function('create', ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, tag=tag, route_name=route_name) # Build the expected string for comparison # A value of None will default to an empty string, and # add the tag or name keywords where appropriate cmd_next_hop_ip = cmd_distance = cmd_tag = cmd_route_name = '' if next_hop_ip is not None: cmd_next_hop_ip = " %s" % next_hop_ip if distance is not None: cmd_distance = " %d" % distance if tag is not None: cmd_tag = " tag %d" % tag if route_name is not None: cmd_route_name = " name %s" % route_name cmds = "ip route %s %s%s%s%s%s" % \ (ip_dest, next_hop, cmd_next_hop_ip, cmd_distance, cmd_tag, cmd_route_name) self.eapi_positive_config_test(func, cmds) def test_delete(self): # Test passing in a full set of parameters to 'delete' # Some parameters may be not set: None for ip_dest in IP_DESTS: (next_hop, next_hop_ip) = choice(NEXT_HOPS) distance = choice(DISTANCES) if distance: distance = random_int(0, 255) tag = choice(TAGS) if tag: tag = random_int(0, 255) route_name = choice(ROUTE_NAMES) if route_name: route_name = random_string(minchar=4, maxchar=10) func = function('delete', ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, tag=tag, route_name=route_name) # Build the expected string for comparison # A value of None will default to an empty string, and # add the tag or name keywords where appropriate cmd_next_hop_ip = cmd_distance = cmd_tag = cmd_route_name = '' if next_hop_ip is not None: cmd_next_hop_ip = " %s" % next_hop_ip if distance is not None: cmd_distance = " %d" % distance if tag is not None: cmd_tag = " tag %d" % tag if route_name is not None: cmd_route_name = " name %s" % route_name cmds = "no ip route %s %s%s%s%s%s" % \ (ip_dest, next_hop, cmd_next_hop_ip, cmd_distance, cmd_tag, cmd_route_name) self.eapi_positive_config_test(func, cmds) def test_default(self): # Test passing in a full set of parameters to 'default' # Some parameters may be not set: None for ip_dest in IP_DESTS: (next_hop, next_hop_ip) = choice(NEXT_HOPS) distance = choice(DISTANCES) if distance: distance = random_int(0, 255) tag = choice(TAGS) if tag: tag = random_int(0, 255) route_name = choice(ROUTE_NAMES) if route_name: route_name = random_string(minchar=4, maxchar=10) func = function('default', ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, tag=tag, route_name=route_name) # Build the expected string for comparison # A value of None will default to an empty string, and # add the tag or name keywords where appropriate cmd_next_hop_ip = cmd_distance = cmd_tag = cmd_route_name = '' if next_hop_ip is not None: cmd_next_hop_ip = " %s" % next_hop_ip if distance is not None: cmd_distance = " %d" % distance if tag is not None: cmd_tag = " tag %d" % tag if route_name is not None: cmd_route_name = " name %s" % route_name cmds = "default ip route %s %s%s%s%s%s" % \ (ip_dest, next_hop, cmd_next_hop_ip, cmd_distance, cmd_tag, cmd_route_name) self.eapi_positive_config_test(func, cmds) def test_set_tag(self): # Test passing in a new tag to the set_tag function ip_dest = '1.2.3.0/24' next_hop = 'Ethernet1' next_hop_ip = '1.1.1.1' distance = 10 tag = '99' func = function('set_tag', ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, tag=tag) cmd = "ip route %s %s %s %s tag %s" % \ (ip_dest, next_hop, next_hop_ip, distance, tag) self.eapi_positive_config_test(func, cmd) def test_set_route_name(self): # Test passing in a new tag to the set_tag function ip_dest = '1.2.3.0/24' next_hop = 'Ethernet1' next_hop_ip = '1.1.1.1' distance = 10 route_name = 'test99' func = function('set_route_name', ip_dest, next_hop, next_hop_ip=next_hop_ip, distance=distance, route_name=route_name) cmd = "ip route %s %s %s %s name %s" % \ (ip_dest, next_hop, next_hop_ip, distance, route_name) self.eapi_positive_config_test(func, cmd) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_stp.py0000644000076500000240000001677614447405743021202 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, random_string, function from testlib import EapiConfigUnitTest import pyeapi.api.stp def get_running_config(): return get_fixture('running_config.text') class TestApiStp(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiStp, self).__init__(*args, **kwargs) self.instance = pyeapi.api.stp.Stp(None) self.config = open(get_running_config()).read() def test_instance(self): result = pyeapi.api.stp.instance(None) self.assertIsInstance(result, pyeapi.api.stp.Stp) def test_interfaces(self): result = self.instance.interfaces self.assertIsInstance(result, pyeapi.api.stp.StpInterfaces) def test_instances(self): result = self.instance.instances self.assertIsInstance(result, pyeapi.api.stp.StpInstances) def test_set_mode_with_value(self): for value in ['mstp', 'none']: cmds = 'spanning-tree mode %s' % value func = function('set_mode', value) self.eapi_positive_config_test(func, cmds) def test_set_mode_with_default(self): cmds = 'default spanning-tree mode' func = function('set_mode', default=True) self.eapi_positive_config_test(func, cmds) def test_set_mode_with_disable(self): cmds = 'no spanning-tree mode' func = function('set_mode', disable=True) self.eapi_positive_config_test(func, cmds) def test_set_mode_invalid_value_raises_value_error(self): value = random_string() func = function('set_mode', value) self.eapi_exception_config_test(func, ValueError) class TestApiStpInterfaces(EapiConfigUnitTest): INTERFACES = ['Ethernet1', 'Ethernet1/1', 'Port-Channel1'] def __init__(self, *args, **kwargs): super(TestApiStpInterfaces, self).__init__(*args, **kwargs) self.instance = pyeapi.api.stp.StpInterfaces(None) self.config = open(get_running_config()).read() def test_getall(self): result = self.instance.getall() self.assertIsInstance(result, dict) def test_set_portfast_type_with_value(self): for intf in self.INTERFACES: for value in ['edge', 'network', 'normal']: cmds = ['interface %s' % intf] cmds.append('spanning-tree portfast %s' % value) if value == 'edge': cmds.append('spanning-tree portfast auto') func = function('set_portfast_type', intf, value) self.eapi_positive_config_test(func, cmds) def test_set_portfast_type_with_no_value(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'spanning-tree portfast normal'] func = function('set_portfast_type', intf) self.eapi_positive_config_test(func, cmds) def test_set_portfast_type_invalid_value_raises_value_error(self): for intf in self.INTERFACES: value = random_string() func = function('set_portfast_type', intf, value) self.eapi_exception_config_test(func, ValueError) def test_set_portfast_type_invalid_intf_raises_value_error(self): intf = random_string() func = function('set_portfast_type', intf) self.eapi_exception_config_test(func, ValueError) def test_set_bpduguard_with_value(self): for intf in self.INTERFACES: for value in ['enable', 'disable']: cfgvalue = value == 'enable' cmds = ['interface %s' % intf, 'spanning-tree bpduguard %s' % value] func = function('set_bpduguard', intf, cfgvalue) self.eapi_positive_config_test(func, cmds) def test_set_bpduguard_with_no_value(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'spanning-tree bpduguard disable'] func = function('set_bpduguard', intf) self.eapi_positive_config_test(func, cmds) def test_set_bpduguard_with_default(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'default spanning-tree bpduguard'] func = function('set_bpduguard', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_bpduguard_with_disable(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no spanning-tree bpduguard'] func = function('set_bpduguard', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_bpduguard_invalid_intf_raises_value_error(self): intf = random_string() func = function('set_bpduguard', intf) self.eapi_exception_config_test(func, ValueError) def test_set_portfast_with_value(self): for intf in self.INTERFACES: for value in [True, False]: cmds = ['interface %s' % intf] if value: cmds.append('spanning-tree portfast') else: cmds.append('no spanning-tree portfast') func = function('set_portfast', intf, value) self.eapi_positive_config_test(func, cmds) def test_set_portfast_with_no_value(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no spanning-tree portfast'] func = function('set_portfast', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_portfast_with_default(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'default spanning-tree portfast'] func = function('set_portfast', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_portfast_invalid_intf_raises_value_error(self): intf = random_string() func = function('set_portfast', intf) self.eapi_exception_config_test(func, ValueError) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_switchports.py0000644000076500000240000002130114447405743022741 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, random_vlan, function from testlib import EapiConfigUnitTest import pyeapi.api.switchports class TestApiSwitchports(EapiConfigUnitTest): INTERFACES = ['Ethernet1', 'Ethernet1/1', 'Port-Channel1'] def __init__(self, *args, **kwargs): super(TestApiSwitchports, self).__init__(*args, **kwargs) self.instance = pyeapi.api.switchports.instance(None) self.config = open(get_fixture('running_config.text')).read() def test_get(self): result = self.instance.get('Ethernet1') keys = ['name', 'mode', 'access_vlan', 'trunk_native_vlan', 'trunk_allowed_vlans', 'trunk_groups'] self.assertEqual(sorted(result.keys()), sorted(keys)) def test_getall(self): expected = sorted(['Port-Channel10', 'Ethernet1', 'Ethernet2', 'Ethernet3', 'Ethernet4', 'Ethernet5', 'Ethernet6', 'Ethernet7', 'Ethernet8']) result = self.instance.getall() self.assertIsInstance(result, dict) self.assertEqual(sorted(self.instance.getall().keys()), expected) def test_instance_functions(self): for intf in self.INTERFACES: for name in ['create', 'delete', 'default']: if name == 'create': cmds = ['interface %s' % intf, 'no ip address', 'switchport'] elif name == 'delete': cmds = ['interface %s' % intf, 'no switchport'] elif name == 'default': cmds = ['interface %s' % intf, 'no ip address', 'default switchport'] func = function(name, intf) self.eapi_positive_config_test(func, cmds) def test_set_mode(self): for intf in self.INTERFACES: for mode in ['access', 'trunk']: cmds = ['interface %s' % intf, 'switchport mode %s' % mode] func = function('set_mode', intf, mode) self.eapi_positive_config_test(func, cmds) def test_set_mode_with_no_value(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no switchport mode'] func = function('set_mode', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_mode_with_default(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'default switchport mode'] func = function('set_mode', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_access_vlan(self): for intf in self.INTERFACES: vid = random_vlan() cmds = ['interface %s' % intf, 'switchport access vlan %s' % vid] func = function('set_access_vlan', intf, vid) self.eapi_positive_config_test(func, cmds) def test_set_access_vlan_with_no_value(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no switchport access vlan'] func = function('set_access_vlan', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_access_vlan_with_default(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'default switchport access vlan'] func = function('set_access_vlan', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_trunk_native_vlan(self): for intf in self.INTERFACES: vid = random_vlan() cmds = ['interface %s' % intf, 'switchport trunk native vlan %s' % vid] func = function('set_trunk_native_vlan', intf, vid) self.eapi_positive_config_test(func, cmds) def test_set_trunk_native_vlan_with_no_value(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no switchport trunk native vlan'] func = function('set_trunk_native_vlan', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_trunk_native_vlan_with_default(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'default switchport trunk native vlan'] func = function('set_trunk_native_vlan', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_trunk_allowed_vlans(self): for intf in self.INTERFACES: vid = '1,2,3-5,6,7' cmds = ['interface %s' % intf, 'switchport trunk allowed vlan %s' % vid] func = function('set_trunk_allowed_vlans', intf, vid) self.eapi_positive_config_test(func, cmds) def test_set_trunk_allowed_vlans_with_no_value(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no switchport trunk allowed vlan'] func = function('set_trunk_allowed_vlans', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_allowed_vlans_with_default(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'default switchport trunk allowed vlan'] func = function('set_trunk_allowed_vlans', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_trunk_groups_with_default(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'default switchport trunk group'] func = function('set_trunk_groups', intf, default=True) self.eapi_positive_config_test(func, cmds) def test_set_trunk_groups_with_no(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no switchport trunk group'] func = function('set_trunk_groups', intf, disable=True) self.eapi_positive_config_test(func, cmds) def test_set_trunk_groups_with_add(self): intf = 'Ethernet1' cmds = ['interface %s' % intf, 'switchport trunk group bang'] func = function('set_trunk_groups', intf, ['foo', 'bar', 'bang']) self.eapi_positive_config_test(func, cmds) def test_set_trunk_groups_with_remove(self): intf = 'Ethernet1' cmds = ['interface %s' % intf, 'no switchport trunk group foo'] func = function('set_trunk_groups', intf, ['bar']) self.eapi_positive_config_test(func, cmds) def test_add_trunk_group(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'switchport trunk group foo'] func = function('add_trunk_group', intf, 'foo') self.eapi_positive_config_test(func, cmds) def test_remove_trunk_group(self): for intf in self.INTERFACES: cmds = ['interface %s' % intf, 'no switchport trunk group foo'] func = function('remove_trunk_group', intf, 'foo') self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_system.py0000644000076500000240000001030414447405743021675 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, random_string, function from testlib import EapiConfigUnitTest import pyeapi.api.system class TestApiSystem(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiSystem, self).__init__(*args, **kwargs) self.instance = pyeapi.api.system.instance(None) self.config = open(get_fixture('running_config.text')).read() def test_get(self): keys = ['hostname', 'iprouting', 'banner_motd', 'banner_login'] result = self.instance.get() self.assertEqual(sorted(keys), sorted(list(result.keys()))) self.assertIsNotNone(self.instance.get()['banner_motd']) self.assertIsNotNone(self.instance.get()['banner_login']) def test_set_hostname(self): for state in ['config', 'negate', 'default']: value = random_string() if state == 'config': cmds = 'hostname %s' % value func = function('set_hostname', value) elif state == 'negate': cmds = 'no hostname' func = function('set_hostname', disable=True) elif state == 'default': cmds = 'default hostname' func = function('set_hostname', value=value, default=True) self.eapi_positive_config_test(func, cmds) def test_set_iprouting(self): for state in ['config', 'negate', 'default']: if state == 'config': cmds = 'ip routing' func = function('set_iprouting', True) elif state == 'negate': cmds = 'no ip routing' func = function('set_iprouting', disable=True) elif state == 'default': cmds = 'default ip routing' func = function('set_iprouting', default=True) self.eapi_positive_config_test(func, cmds) def test_set_banner(self): banner_value = random_string() + "\n" func = function('set_banner', banner_type='motd', value=banner_value) cmds = [dict(cmd='banner motd', input=banner_value)] self.eapi_positive_config_test(func, cmds) def test_set_banner_default_disable(self): func = function('set_banner', banner_type='motd', value=None, default=True) cmds = 'default banner motd' self.eapi_positive_config_test(func, cmds) func = function('set_banner', banner_type='motd', value=None, disable=True) cmds = 'no banner motd' self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_users.py0000644000076500000240000001350314447405743021516 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.users class TestApiUsers(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiUsers, self).__init__(*args, **kwargs) self.instance = pyeapi.api.users.instance(None) self.config = open(get_fixture('running_config.text')).read() def test_isprivilege_returns_false(self): result = pyeapi.api.users.isprivilege('test') self.assertFalse(result) def test_get(self): keys = ['nopassword', 'privilege', 'role', 'secret', 'format', 'sshkey'] result = self.instance.get('test') self.assertEqual(sorted(keys), sorted(result.keys())) def test_getall(self): result = self.instance.getall() self.assertIsInstance(result, dict) def test_create_with_nopassword(self): cmds = 'username test nopassword' func = function('create', 'test', nopassword=True) self.eapi_positive_config_test(func, cmds) def test_create_with_secret_cleartext(self): cmds = 'username test secret 0 pass' func = function('create', 'test', secret='pass') self.eapi_positive_config_test(func, cmds) def test_create_with_secret_md5(self): cmds = 'username test secret 5 pass' func = function('create', 'test', secret='pass', encryption='md5') self.eapi_positive_config_test(func, cmds) def test_create_with_secret_nologin(self): cmds = 'username test secret *' func = function('create', 'test', secret='', encryption='nologin') self.eapi_positive_config_test(func, cmds) def test_create_with_secret_sha512(self): cmds = 'username test secret sha512 pass' func = function('create', 'test', secret='pass', encryption='sha512') self.eapi_positive_config_test(func, cmds) def test_create_with_missing_kwargs(self): with self.assertRaises(TypeError): self.instance.create('test') def test_create_with_invalid_secret_arg(self): with self.assertRaises(TypeError): self.instance.create_with_secret('test', 'test', 'test') def test_delete(self): with self.assertRaises(TypeError): self.instance.delete('admin') def test_delete_admin_exception(self): cmds = 'no username test' func = function('delete', 'test') self.eapi_positive_config_test(func, cmds) def test_default(self): cmds = 'default username test' func = function('default', 'test') self.eapi_positive_config_test(func, cmds) def test_set_privilege(self): cmds = 'username test privilege 8' func = function('set_privilege', 'test', 8) self.eapi_positive_config_test(func, cmds) def test_set_privilege_negate(self): cmds = 'username test privilege 1' func = function('set_privilege', 'test') self.eapi_positive_config_test(func, cmds) def test_set_privilege_invalid_value(self): with self.assertRaises(TypeError): self.instance.set_privilege('test', 16) def test_set_role(self): cmds = 'username test role ops' func = function('set_role', 'test', value='ops') self.eapi_positive_config_test(func, cmds) def test_set_role_negate(self): cmds = 'no username test role' func = function('set_role', 'test', disable=True) self.eapi_positive_config_test(func, cmds) def test_set_role_default(self): cmds = 'default username test role' func = function('set_role', 'test', default=True) self.eapi_positive_config_test(func, cmds) def test_set_sshkey(self): cmds = 'username test sshkey newkey' func = function('set_sshkey', 'test', value='newkey') self.eapi_positive_config_test(func, cmds) def test_set_sshkey_negate(self): cmds = 'no username test sshkey' func = function('set_sshkey', 'test', disable=True) self.eapi_positive_config_test(func, cmds) def test_set_sshkey_default(self): cmds = 'default username test sshkey' func = function('set_sshkey', 'test', default=True) self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_varp.py0000644000076500000240000001373714447405743021336 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.varp class TestApiVarp(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiVarp, self).__init__(*args, **kwargs) self.instance = pyeapi.api.varp.Varp(None) self.config = open(get_fixture('running_config.varp')).read() def test_instance(self): result = pyeapi.api.varp.instance(None) self.assertIsInstance(result, pyeapi.api.varp.Varp) def test_get(self): result = self.instance.get() keys = ['mac_address', 'interfaces'] self.assertEqual(sorted(keys), sorted(result.keys())) self.assertIsNotNone(self.instance.get()['mac_address']) self.assertIsNotNone(self.instance.get()['interfaces']) def test_get_interfaces_none(self): self._interfaces = None result = self.instance.interfaces() self.assertIsNotNone(result) def test_get_interfaces_already_defined(self): self.instance.interfaces() result = self.instance.interfaces() self.assertIsNotNone(result) def test_set_mac_address_with_value(self): value = 'aa:bb:cc:dd:ee:ff' func = function('set_mac_address', mac_address=value) cmds = 'ip virtual-router mac-address %s' % value self.eapi_positive_config_test(func, cmds) def test_set_mac_address_with_positional_value(self): value = 'aa:bb:cc:dd:ee:ff' func = function('set_mac_address', value) cmds = 'ip virtual-router mac-address %s' % value self.eapi_positive_config_test(func, cmds) def test_set_mac_address_with_disable(self): func = function('set_mac_address', disable=True) cmds = 'no ip virtual-router mac-address 00:11:22:33:44:55' self.eapi_positive_config_test(func, cmds) def test_set_mac_address_with_no_value(self): with self.assertRaises(ValueError): self.instance.set_mac_address(mac_address=None) def test_set_mac_address_with_bad_value(self): with self.assertRaises(ValueError): self.instance.set_mac_address(mac_address='0011.2233.4455') def test_set_mac_address_with_default(self): func = function('set_mac_address', default=True) cmds = 'default ip virtual-router mac-address 00:11:22:33:44:55' self.eapi_positive_config_test(func, cmds) class TestApiVarpInterfaces(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiVarpInterfaces, self).__init__(*args, **kwargs) self.instance = pyeapi.api.varp.VarpInterfaces(None) self.config = open(get_fixture('running_config.varp')).read() def test_get_with_no_interface(self): self.config = "" self.setUp() result = self.instance.get('Vlan1000') self.assertIsNone(result) def test_add_address_with_value(self): func = function('set_addresses', 'Vlan4001', addresses=['1.1.1.4']) cmds = ['interface Vlan4001', 'no ip virtual-router address 1.1.1.2', 'ip virtual-router address 1.1.1.4'] self.eapi_positive_config_test(func, cmds) def test_add_address_when_interface_does_not_exist(self): self.config = "" self.setUp() func = function('set_addresses', 'Vlan10', addresses=['1.1.1.4']) cmds = ['interface Vlan10', 'ip virtual-router address 1.1.1.4'] self.eapi_positive_config_test(func, cmds) def test_add_address_with_no_value(self): func = function('set_addresses', 'Vlan4002') cmds = ['interface Vlan4002', 'no ip virtual-router address'] self.eapi_positive_config_test(func, cmds) def test_add_address_with_empty_list(self): func = function('set_addresses', 'Vlan4001', addresses=[]) cmds = ['interface Vlan4001', 'no ip virtual-router address 1.1.1.2'] self.eapi_positive_config_test(func, cmds) def test_add_address_with_default(self): func = function('set_addresses', 'Vlan4001', default=True) cmds = ['interface Vlan4001', 'default ip virtual-router address'] self.eapi_positive_config_test(func, cmds) def test_add_address_with_disable(self): func = function('set_addresses', 'Vlan4001', disable=True) cmds = ['interface Vlan4001', 'no ip virtual-router address'] self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_vlans.py0000644000076500000240000001441214447405743021500 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, random_vlan, random_string, function from testlib import EapiConfigUnitTest import pyeapi.api.vlans class TestApiVlans(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiVlans, self).__init__(*args, **kwargs) self.instance = pyeapi.api.vlans.instance(None) self.config = open(get_fixture('running_config.text')).read() def test_isvlan_with_string(self): self.assertFalse(pyeapi.api.vlans.isvlan('a' + random_string())) def test_isvlan_valid_value(self): self.assertTrue(pyeapi.api.vlans.isvlan('1234')) def test_isvlan_invalid_value(self): self.assertFalse(pyeapi.api.vlans.isvlan('5000')) def test_get(self): result = self.instance.get('1') vlan = dict(vlan_id='1', name='default', state='active', trunk_groups=[]) self.assertEqual(vlan, result) # ensure capturing grouppped vlans result = self.instance.get('200-.*') vlan = dict(vlan_id='200-202,204', name='grouping', state='active', trunk_groups=[]) self.assertEqual(vlan, result) def test_get_not_configured(self): self.assertIsNone(self.instance.get('1000')) def test_getall(self): result = self.instance.getall() self.assertIsInstance(result, dict) self.assertEqual(len(result), 5) def test_vlan_functions(self): for name in ['create', 'delete', 'default']: vid = random_vlan() if name == 'create': cmds = 'vlan %s' % vid elif name == 'delete': cmds = 'no vlan %s' % vid elif name == 'default': cmds = 'default vlan %s' % vid func = function(name, vid) self.eapi_positive_config_test(func, cmds) def test_set_name(self): for state in ['config', 'negate', 'default']: vid = random_vlan() name = random_string() if state == 'config': cmds = ['vlan %s' % vid, 'name %s' % name] func = function('set_name', vid, name) elif state == 'negate': cmds = ['vlan %s' % vid, 'no name'] func = function('set_name', vid, disable=True) elif state == 'default': cmds = ['vlan %s' % vid, 'default name'] func = function('set_name', vid, default=True) self.eapi_positive_config_test(func, cmds) def test_set_state(self): for state in ['config', 'negate', 'default']: vid = random_vlan() if state == 'config': for value in ['active', 'suspend']: cmds = ['vlan %s' % vid, 'state %s' % value] func = function('set_state', vid, value) self.eapi_positive_config_test(func, cmds) elif state == 'negate': cmds = ['vlan %s' % vid, 'no state'] func = function('set_state', vid, disable=True) self.eapi_positive_config_test(func, cmds) elif state == 'default': cmds = ['vlan %s' % vid, 'default state'] func = function('set_state', vid, default=True) self.eapi_positive_config_test(func, cmds) def test_set_trunk_groups_default(self): vid = random_vlan() cmds = ['vlan %s' % vid, 'default trunk group'] func = function('set_trunk_groups', vid, default=True) self.eapi_positive_config_test(func, cmds) def test_set_trunk_groups_add_value(self): cmds = ['vlan 10', 'trunk group tg2'] func = function('set_trunk_groups', '10', ['tg1', 'tg2']) self.eapi_positive_config_test(func, cmds) def test_set_trunk_groups_remove_value(self): cmds = ['vlan 10', 'no trunk group tg1'] func = function('set_trunk_groups', '10', 'tg2') self.eapi_positive_config_test(func, cmds) def test_set_trunk_groups_remove_all(self): cmds = ['vlan 10', 'no trunk group'] func = function('set_trunk_groups', '10', disable=True) self.eapi_positive_config_test(func, cmds) def test_add_trunk_group(self): vid = random_vlan() tg = random_string() cmds = ['vlan %s' % vid, 'trunk group %s' % tg] func = function('add_trunk_group', vid, tg) self.eapi_positive_config_test(func, cmds) def test_remove_trunk_group(self): vid = random_vlan() tg = random_string() cmds = ['vlan %s' % vid, 'no trunk group %s' % tg] func = function('remove_trunk_group', vid, tg) self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_vrfs.py0000644000076500000240000001625214447405743021341 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2017, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.vrfs class TestApiVrfs(EapiConfigUnitTest): def __init__(self, *args, **kwargs): super(TestApiVrfs, self).__init__(*args, **kwargs) self.instance = pyeapi.api.vrfs.instance(None) self.config = open(get_fixture('running_config.vrf')).read() def test_get(self): result = self.instance.get('blah') vrf = dict(rd='10:10', vrf_name='blah', description='blah desc', ipv4_routing=True, ipv6_routing=False) self.assertEqual(vrf, result) result2 = self.instance.get('test') vrf2 = dict(rd='200:500', vrf_name='test', description='', ipv4_routing=False, ipv6_routing=True) self.assertEqual(vrf2, result2) def test_get_not_configured(self): self.assertIsNone(self.instance.get('notthere')) def test_getall(self): result = self.instance.getall() self.assertIsInstance(result, dict) self.assertEqual(len(result), 3) def test_vrf_functions(self): for name in ['create', 'delete', 'default']: vrf_name = 'testvrf' if name == 'create': cmds = ['vrf definition %s' % vrf_name] elif name == 'delete': cmds = 'no vrf definition %s' % vrf_name elif name == 'default': cmds = 'default vrf definition %s' % vrf_name func = function(name, vrf_name) self.eapi_positive_config_test(func, cmds) def test_vrf_create_with_rd(self): vrf_name = 'testvrfrd' rd = '10:10' cmds = ['vrf definition %s' % vrf_name, 'rd %s' % rd] func = function('create', vrf_name, rd=rd) self.eapi_positive_config_test(func, cmds) def test_set_rd(self): vrf_name = 'testrdvrf' rd = '10:10' cmds = ['vrf definition %s' % vrf_name, 'rd %s' % rd] func = function('set_rd', vrf_name, rd) self.eapi_positive_config_test(func, cmds) def test_set_description(self): for state in ['config', 'negate', 'default']: vrf_name = 'testdescvrf' if state == 'config': description = 'testing' cmds = ['vrf definition %s' % vrf_name, 'description %s' % description] func = function('set_description', vrf_name, description) self.eapi_positive_config_test(func, cmds) elif state == 'negate': cmds = ['vrf definition %s' % vrf_name, 'no description'] func = function('set_description', vrf_name, disable=True) self.eapi_positive_config_test(func, cmds) elif state == 'default': cmds = ['vrf definition %s' % vrf_name, 'default description'] func = function('set_description', vrf_name, default=True) self.eapi_positive_config_test(func, cmds) def test_set_ipv4_routing(self): for state in ['config', 'negate', 'default']: vrf_name = 'testipv4vrf' if state == 'config': cmds = ['ip routing vrf %s' % vrf_name] func = function('set_ipv4_routing', vrf_name) self.eapi_positive_config_test(func, cmds) elif state == 'negate': cmds = ['no ip routing vrf %s' % vrf_name] func = function('set_ipv4_routing', vrf_name, disable=True) self.eapi_positive_config_test(func, cmds) elif state == 'default': cmds = ['default ip routing vrf %s' % vrf_name] func = function('set_ipv4_routing', vrf_name, default=True) self.eapi_positive_config_test(func, cmds) def test_set_ipv6_routing(self): for state in ['config', 'negate', 'default']: vrf_name = 'testipv6vrf' if state == 'config': cmds = ['ipv6 unicast-routing vrf %s' % vrf_name] func = function('set_ipv6_routing', vrf_name) self.eapi_positive_config_test(func, cmds) elif state == 'negate': cmds = ['no ipv6 unicast-routing vrf %s' % vrf_name] func = function('set_ipv6_routing', vrf_name, disable=True) self.eapi_positive_config_test(func, cmds) elif state == 'default': cmds = ['default ipv6 unicast-routing vrf %s' % vrf_name] func = function('set_ipv6_routing', vrf_name, default=True) self.eapi_positive_config_test(func, cmds) def test_set_interface(self): for state in ['config', 'negate', 'default']: vrf_name = 'testintvrf' interface = 'Ethernet1' if state == 'config': cmds = ['interface %s' % interface, 'vrf forwarding %s' % vrf_name] func = function('set_interface', vrf_name, interface) self.eapi_positive_config_test(func, cmds) elif state == 'negate': cmds = ['interface %s' % interface, 'no vrf forwarding'] func = function('set_interface', vrf_name, interface, disable=True) self.eapi_positive_config_test(func, cmds) elif state == 'default': cmds = ['interface %s' % interface, 'default vrf forwarding'] func = function('set_interface', vrf_name, interface, default=True) self.eapi_positive_config_test(func, cmds) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_api_vrrp.py0000644000076500000240000007444314447405743021360 0ustar dlyssenkostaff00000000000000# # right (c) 2015, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from testlib import get_fixture, function from testlib import EapiConfigUnitTest import pyeapi.api.vrrp upd_intf = 'Vlan50' upd_vrid = 10 upd_cmd = 'interface %s' % upd_intf known_vrrps = { 'Ethernet1': { 10: {'priority': 175, 'timers_advertise': 1, 'mac_addr_adv_interval': 30, 'preempt': True, 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'primary_ip': '10.10.6.10', 'secondary_ip': [], 'description': 'vrrp 10 on Ethernet1', 'enable': True, 'track': [], 'bfd_ip': '', 'ip_version': 2} }, 'Port-Channel10': { 10: {'priority': 150, 'timers_advertise': 1, 'mac_addr_adv_interval': 30, 'preempt': True, 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'primary_ip': '10.10.5.10', 'secondary_ip': ['10.10.5.20'], 'description': 'vrrp 10 on Port-Channel10', 'enable': True, 'track': [], 'bfd_ip': '', 'ip_version': 2} }, 'Vlan50': { 10: {'priority': 200, 'timers_advertise': 3, 'mac_addr_adv_interval': 30, 'preempt': True, 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'primary_ip': '10.10.4.10', 'secondary_ip': ['10.10.4.21', '10.10.4.22', '10.10.4.23', '10.10.4.24'], 'description': '', 'enable': True, 'track': [ {'name': 'Ethernet1', 'action': 'decrement', 'amount': 10}, {'name': 'Ethernet1', 'action': 'shutdown'}, {'name': 'Ethernet2', 'action': 'decrement', 'amount': 50}, {'name': 'Ethernet2', 'action': 'shutdown'}, {'name': 'Ethernet11', 'action': 'decrement', 'amount': 75}, {'name': 'Ethernet11', 'action': 'shutdown'}, ], 'bfd_ip': '', 'ip_version': 2}, 20: {'priority': 100, 'timers_advertise': 5, 'mac_addr_adv_interval': 30, 'preempt': False, 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'primary_ip': '10.10.4.20', 'secondary_ip': [], 'description': '', 'enable': False, 'track': [ {'name': 'Ethernet1', 'action': 'shutdown'}, {'name': 'Ethernet2', 'action': 'decrement', 'amount': 1}, {'name': 'Ethernet2', 'action': 'shutdown'}, ], 'bfd_ip': '', 'ip_version': 2}, 30: {'priority': 50, 'timers_advertise': 1, 'mac_addr_adv_interval': 30, 'preempt': True, 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'primary_ip': '10.10.4.30', 'secondary_ip': [], 'description': '', 'enable': True, 'track': [], 'bfd_ip': '10.10.4.33', 'ip_version': 2} } } class TestApiVrrp(EapiConfigUnitTest): maxDiff = None def __init__(self, *args, **kwargs): super(TestApiVrrp, self).__init__(*args, **kwargs) self.instance = pyeapi.api.vrrp.Vrrp(None) self.config = open(get_fixture('running_config.vrrp')).read() def test_instance(self): result = pyeapi.api.vrrp.instance(None) self.assertIsInstance(result, pyeapi.api.vrrp.Vrrp) def test_get(self): # Request various sets of vrrp configurations for interface in known_vrrps: known = known_vrrps.get(interface) for vrid in known: known[vrid] = self.instance.vrconf_format(known[vrid]) result = self.instance.get(interface) self.assertEqual(result, known) def test_get_non_existent_interface(self): # Request vrrp configuration for an interface that # is not defined result = self.instance.get('Vlan2000') self.assertIsNone(result) def test_get_invalid_parameters(self): # Pass empty, None, or other invalid parameters to get() with self.assertRaises(ValueError): self.instance.get('') with self.assertRaises(ValueError): self.instance.get(None) def test_getall(self): # Get all the vrrp configurations from the config result = self.instance.getall() self.assertEqual(result, known_vrrps) def test_create(self): interface = 'Ethernet1' vrid = 10 # Test create with a normal configuration configuration = { 'primary_ip': '10.10.60.10', 'priority': 200, 'description': 'modified vrrp 10 on Ethernet1', 'secondary_ip': ['10.10.60.20', '10.10.60.30'], 'ip_version': 3, 'timers_advertise': 2, 'mac_addr_adv_interval': 3, 'preempt': True, 'preempt_delay_min': 1, 'preempt_delay_reload': 1, 'delay_reload': 1, 'track': [ {'name': 'Ethernet1', 'action': 'decrement', 'amount': 1}, {'name': 'Ethernet1', 'action': 'shutdown'}, {'name': 'Ethernet2', 'action': 'decrement', 'amount': 1}, {'name': 'Ethernet2', 'action': 'shutdown'}, ], 'bfd_ip': '10.10.60.30', } cmds = [ 'interface Ethernet1', 'vrrp 10 shutdown', 'vrrp 10 ip 10.10.60.10', 'vrrp 10 priority 200', 'vrrp 10 description modified vrrp 10 on Ethernet1', 'vrrp 10 ip version 3', 'vrrp 10 ip 10.10.60.20 secondary', 'vrrp 10 ip 10.10.60.30 secondary', 'vrrp 10 timers advertise 2', 'vrrp 10 mac-address advertisement-interval 3', 'vrrp 10 preempt', 'vrrp 10 preempt delay minimum 1', 'vrrp 10 preempt delay reload 1', 'vrrp 10 delay reload 1', 'vrrp 10 track Ethernet1 decrement 1', 'vrrp 10 track Ethernet1 shutdown', 'vrrp 10 track Ethernet2 decrement 1', 'vrrp 10 track Ethernet2 shutdown', 'vrrp 10 bfd ip 10.10.60.30', ] func = function('create', interface, vrid, **configuration) self.eapi_positive_config_test(func, cmds) # Test create setting possible parameters to 'no' configuration = { 'primary_ip': 'no', 'priority': 'no', 'description': 'no', 'secondary_ip': [], 'ip_version': 'no', 'timers_advertise': 'no', 'mac_addr_adv_interval': 'no', 'preempt': 'no', 'preempt_delay_min': 'no', 'preempt_delay_reload': 'no', 'delay_reload': 'no', 'track': [], 'bfd_ip': 'no', } cmds = [ 'interface Ethernet1', 'vrrp 10 shutdown', 'no vrrp 10 ip 10.10.6.10', 'no vrrp 10 priority', 'no vrrp 10 description', 'no vrrp 10 ip version', 'no vrrp 10 timers advertise', 'no vrrp 10 mac-address advertisement-interval', 'no vrrp 10 preempt', 'no vrrp 10 preempt delay minimum', 'no vrrp 10 preempt delay reload', 'no vrrp 10 delay reload', 'no vrrp 10 bfd ip', ] func = function('create', interface, vrid, **configuration) self.eapi_positive_config_test(func, cmds) # Test create setting possible parameters to 'default' configuration = { 'primary_ip': 'default', 'priority': 'default', 'description': 'default', 'secondary_ip': [], 'ip_version': 'default', 'timers_advertise': 'default', 'mac_addr_adv_interval': 'default', 'preempt': 'default', 'preempt_delay_min': 'default', 'preempt_delay_reload': 'default', 'delay_reload': 'default', 'track': [], 'bfd_ip': 'default', } cmds = [ 'interface Ethernet1', 'vrrp 10 shutdown', 'default vrrp 10 ip 10.10.6.10', 'default vrrp 10 priority', 'default vrrp 10 description', 'default vrrp 10 ip version', 'default vrrp 10 timers advertise', 'default vrrp 10 mac-address advertisement-interval', 'default vrrp 10 preempt', 'default vrrp 10 preempt delay minimum', 'default vrrp 10 preempt delay reload', 'default vrrp 10 delay reload', 'default vrrp 10 bfd ip', ] func = function('create', interface, vrid, **configuration) self.eapi_positive_config_test(func, cmds) def test_delete(self): interface = 'Ethernet1' vrid = 10 cmds = [ 'interface Ethernet1', 'no vrrp 10', ] func = function('delete', interface, vrid) self.eapi_positive_config_test(func, cmds) def test_default(self): interface = 'Ethernet1' vrid = 10 cmds = [ 'interface Ethernet1', 'default vrrp 10', ] func = function('default', interface, vrid) self.eapi_positive_config_test(func, cmds) def test_set_enable(self): # no vrrp 10 shutdown # Test set_enable gives properly formatted commands cases = [ (False, 'vrrp %d shutdown' % upd_vrid), (True, 'no vrrp %d shutdown' % upd_vrid), ] for (enable, cmd) in cases: func = function('set_enable', upd_intf, upd_vrid, value=enable) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError from invalid parameters cases = ['a', 200] for enable in cases: func = function('set_enable', upd_intf, upd_vrid, value=enable) self.eapi_exception_config_test(func, ValueError) def test_set_primary_ip(self): # vrrp 10 ip 10.10.4.10 # Test set_primary_ip gives properly formatted commands ip1 = '10.10.4.110' ipcurr = '10.10.4.10' cases = [ (ip1, None, None, 'vrrp %d ip %s' % (upd_vrid, ip1)), (ip1, True, None, 'no vrrp %d ip %s' % (upd_vrid, ipcurr)), (ip1, None, True, 'default vrrp %d ip %s' % (upd_vrid, ipcurr)), (ip1, True, True, 'default vrrp %d ip %s' % (upd_vrid, ipcurr)), ] for (primary_ip, disable, default, cmd) in cases: func = function('set_primary_ip', upd_intf, upd_vrid, value=primary_ip, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError from invalid parameters cases = ['abc', 500, '101.101'] for primary_ip in cases: func = function('set_primary_ip', upd_intf, upd_vrid, value=primary_ip) self.eapi_exception_config_test(func, ValueError) def test_set_priority(self): # vrrp 10 priority 200 # Test set_primary_ip gives properly formatted commands cases = [ (150, None, None, 'vrrp %d priority 150' % upd_vrid), (None, None, True, 'default vrrp %d priority' % upd_vrid), (None, True, True, 'default vrrp %d priority' % upd_vrid), (None, True, None, 'no vrrp %d priority' % upd_vrid), ] for (priority, disable, default, cmd) in cases: func = function('set_priority', upd_intf, upd_vrid, value=priority, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError from invalid parameters cases = ['abc', 500, False] for priority in cases: func = function('set_priority', upd_intf, upd_vrid, value=priority) self.eapi_exception_config_test(func, ValueError) def test_set_description(self): # no vrrp 10 description desc = 'test description' # Test set_description gives properly formatted commands cases = [ (desc, None, None, 'vrrp %d description %s' % (upd_vrid, desc)), (None, None, True, 'default vrrp %d description' % upd_vrid), (None, True, True, 'default vrrp %d description' % upd_vrid), (None, True, None, 'no vrrp %d description' % upd_vrid), ] for (description, disable, default, cmd) in cases: func = function('set_description', upd_intf, upd_vrid, value=description, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) def test_set_ip_version(self): # vrrp 10 ip version 2 # Test set_description gives properly formatted commands cases = [ (2, None, None, 'vrrp %d ip version 2' % upd_vrid), (None, None, True, 'default vrrp %d ip version' % upd_vrid), (None, True, True, 'default vrrp %d ip version' % upd_vrid), (None, True, None, 'no vrrp %d ip version' % upd_vrid), ] for (ip_version, disable, default, cmd) in cases: func = function('set_ip_version', upd_intf, upd_vrid, value=ip_version, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = ['a', 5] for ip_version in cases: func = function('set_ip_version', upd_intf, upd_vrid, value=ip_version) self.eapi_exception_config_test(func, ValueError) def test_set_secondary_ips(self): # vrrp 10 ip 10.10.4.21 secondary # vrrp 10 ip 10.10.4.22 secondary # vrrp 10 ip 10.10.4.23 secondary curr1 = '10.10.4.21' curr2 = '10.10.4.22' curr3 = '10.10.4.23' curr4 = '10.10.4.24' new1 = '10.10.4.31' new2 = '10.10.4.32' new3 = '10.10.4.33' new4 = curr4 # Test set_secondary_ips gives properly formatted commands cases = [ ([new1, new2, new3], {'add': [new1, new2, new3], 'remove': [curr1, curr2, curr3, curr4]}), ([new1, new2, new4], {'add': [new1, new2], 'remove': [curr1, curr2, curr3]}), ([], {'add': [], 'remove': [curr1, curr2, curr3, curr4]}), ] for (secondary_ips, cmd_dict) in cases: cmds = [] for sec_ip in cmd_dict['add']: cmds.append("vrrp %d ip %s secondary" % (upd_vrid, sec_ip)) for sec_ip in cmd_dict['remove']: cmds.append("no vrrp %d ip %s secondary" % (upd_vrid, sec_ip)) func = function('set_secondary_ips', upd_intf, upd_vrid, secondary_ips) exp_cmds = [upd_cmd] + sorted(cmds) self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = [ [new1, new2, 'abc'], [new1, new2, '10.10.10'], [new1, new2, True], ] for secondary_ips in cases: func = function('set_secondary_ips', upd_intf, upd_vrid, secondary_ips) self.eapi_exception_config_test(func, ValueError) def test_set_timers_advertise(self): # vrrp 10 timers advertise 3 # Test set_timers_advertise gives properly formatted commands cases = [ (50, None, None, 'vrrp %d timers advertise 50' % upd_vrid), (None, None, True, 'default vrrp %d timers advertise' % upd_vrid), (None, True, True, 'default vrrp %d timers advertise' % upd_vrid), (None, True, None, 'no vrrp %d timers advertise' % upd_vrid), ] for (timers_advertise, disable, default, cmd) in cases: func = function('set_timers_advertise', upd_intf, upd_vrid, value=timers_advertise, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = [256, 0, 'a'] for timers_advertise in cases: func = function('set_timers_advertise', upd_intf, upd_vrid, value=timers_advertise) self.eapi_exception_config_test(func, ValueError) def test_set_mac_addr_adv_interval(self): # vrrp 10 mac-address advertisement-interval 30 # Test set_timers_advertise gives properly formatted commands maadvint = 'mac-address advertisement-interval' cases = [ (50, None, None, 'vrrp %d %s 50' % (upd_vrid, maadvint)), (None, None, True, 'default vrrp %d %s' % (upd_vrid, maadvint)), (None, True, True, 'default vrrp %d %s' % (upd_vrid, maadvint)), (None, True, None, 'no vrrp %d %s' % (upd_vrid, maadvint)), ] for (mac_addr_adv_interval, disable, default, cmd) in cases: func = function('set_mac_addr_adv_interval', upd_intf, upd_vrid, value=mac_addr_adv_interval, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = ['a', 10000] for mac_addr_adv_interval in cases: func = function('set_mac_addr_adv_interval', upd_intf, upd_vrid, value=mac_addr_adv_interval) self.eapi_exception_config_test(func, ValueError) def test_set_preempt(self): # vrrp 10 preempt # Test set_description gives properly formatted commands cases = [ (False, None, None, 'no vrrp %d preempt' % upd_vrid), (True, None, None, 'vrrp %d preempt' % upd_vrid), (None, None, True, 'default vrrp %d preempt' % upd_vrid), (None, True, True, 'default vrrp %d preempt' % upd_vrid), (None, True, None, 'no vrrp %d preempt' % upd_vrid), ] for (preempt, disable, default, cmd) in cases: func = function('set_preempt', upd_intf, upd_vrid, value=preempt, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = ['a', 5] for preempt in cases: func = function('set_preempt', upd_intf, upd_vrid, value=preempt) self.eapi_exception_config_test(func, ValueError) def test_set_preempt_delay_min(self): # vrrp 10 preempt delay minimum 0 # Test set_preempt_delay_min gives properly formatted commands cases = [ (2500, None, None, 'vrrp %d preempt delay minimum 2500' % upd_vrid), (None, None, True, 'default vrrp %d preempt delay minimum' % upd_vrid), (None, True, True, 'default vrrp %d preempt delay minimum' % upd_vrid), (None, True, None, 'no vrrp %d preempt delay minimum' % upd_vrid), ] for (preempt_delay_min, disable, default, cmd) in cases: func = function('set_preempt_delay_min', upd_intf, upd_vrid, value=preempt_delay_min, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = ['a', 3601] for preempt_delay_min in cases: func = function('set_preempt_delay_min', upd_intf, upd_vrid, value=preempt_delay_min) self.eapi_exception_config_test(func, ValueError) def test_set_preempt_delay_reload(self): # vrrp 10 preempt delay reload 0 # Test set_preempt_delay_min gives properly formatted commands cases = [ (1500, None, None, 'vrrp %d preempt delay reload 1500' % upd_vrid), (None, None, True, 'default vrrp %d preempt delay reload' % upd_vrid), (None, True, True, 'default vrrp %d preempt delay reload' % upd_vrid), (None, True, None, 'no vrrp %d preempt delay reload' % upd_vrid), ] for (preempt_delay_reload, disable, default, cmd) in cases: func = function('set_preempt_delay_reload', upd_intf, upd_vrid, value=preempt_delay_reload, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = ['a', 3601] for preempt_delay_reload in cases: func = function('set_preempt_delay_reload', upd_intf, upd_vrid, value=preempt_delay_reload) self.eapi_exception_config_test(func, ValueError) def test_set_delay_reload(self): # vrrp 10 delay reload 0 # Test set_delay_min gives properly formatted commands cases = [ (1750, None, None, 'vrrp %d delay reload 1750' % upd_vrid), (None, None, True, 'default vrrp %d delay reload' % upd_vrid), (None, True, True, 'default vrrp %d delay reload' % upd_vrid), (None, True, None, 'no vrrp %d delay reload' % upd_vrid), ] for (delay_reload, disable, default, cmd) in cases: func = function('set_delay_reload', upd_intf, upd_vrid, value=delay_reload, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = ['a', 3601] for delay_reload in cases: func = function('set_delay_reload', upd_intf, upd_vrid, value=delay_reload) self.eapi_exception_config_test(func, ValueError) def test_set_tracks(self): # vrrp 10 track Ethernet1 decrement 10 # vrrp 10 track Ethernet1 shutdown # vrrp 10 track Ethernet2 decrement 50 # vrrp 10 track Ethernet2 shutdown # vrrp 10 track Ethernet11 decrement 75 # vrrp 10 track Ethernet11 shutdown curr1 = {'name': 'Ethernet1', 'action': 'decrement', 'amount': 10} curr2 = {'name': 'Ethernet1', 'action': 'shutdown'} curr3 = {'name': 'Ethernet2', 'action': 'decrement', 'amount': 50} curr4 = {'name': 'Ethernet2', 'action': 'shutdown'} curr5 = {'name': 'Ethernet11', 'action': 'decrement', 'amount': 75} curr6 = {'name': 'Ethernet11', 'action': 'shutdown'} new1 = curr1 new2 = {'name': 'Ethernet2', 'action': 'decrement', 'amount': 49} new3 = {'name': 'Ethernet3', 'action': 'shutdown'} new4 = {'name': 'Ethernet4', 'action': 'decrement', 'amount': 50} new5 = {'name': 'Ethernet5', 'action': 'shutdown'} new6 = {'name': 'Ethernet9', 'action': 'decrement', 'amount': 75} # Test set_track gives properly formatted commands cases = [ ([curr6, curr5, new1, new2], {'add': [new2], 'remove': [curr2, curr3, curr4]}), ([new2, new3, new4, new5, new6], {'add': [new2, new3, new4, new5, new6], 'remove': [curr1, curr2, curr3, curr4, curr5, curr6]}), ([], {'add': [], 'remove': [curr1, curr2, curr3, curr4, curr5, curr6]}), ] for (tracks, cmd_dict) in cases: cmds = [] for add in cmd_dict['add']: tr_obj = add['name'] action = add['action'] amount = add['amount'] if 'amount' in add else '' cmd = ("vrrp %d track %s %s %s" % (upd_vrid, tr_obj, action, amount)) cmds.append(cmd.rstrip()) for remove in cmd_dict['remove']: tr_obj = remove['name'] action = remove['action'] amount = remove['amount'] if 'amount' in remove else '' cmd = ("no vrrp %d track %s %s %s" % (upd_vrid, tr_obj, action, amount)) cmds.append(cmd.rstrip()) func = function('set_tracks', upd_intf, upd_vrid, tracks) exp_cmds = [upd_cmd] + sorted(cmds) self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError by entering invalid parameters cases = [ [{'name': 'Ethernet1', 'action': 'disable', 'amount': 10}], [{'name': 'Ethernet1', 'action': 'decrement', 'amount': True}], [{'name': 'Ethernet1', 'action': 'shutdown', 'amount': 10}], [{'action': 'decrement', 'amount': 10}], [{'name': 'Ethernet1', 'action': 'decrement', 'amount': 10, 'bad': 1}], ] for tracks in cases: func = function('set_tracks', upd_intf, upd_vrid, tracks) self.eapi_exception_config_test(func, ValueError) def test_set_bfd_ip(self): # no vrrp 10 bfd ip bfd_addr = '10.10.4.101' # Test bfd_ip gives properly formatted commands cases = [ (bfd_addr, None, None, 'vrrp %d bfd ip %s' % (upd_vrid, bfd_addr)), (None, True, None, 'no vrrp %d bfd ip' % upd_vrid), (None, None, True, 'default vrrp %d bfd ip' % upd_vrid), (None, True, True, 'default vrrp %d bfd ip' % upd_vrid), ] for (bfd_ip, disable, default, cmd) in cases: func = function('set_bfd_ip', upd_intf, upd_vrid, value=bfd_ip, disable=disable, default=default) exp_cmds = [upd_cmd] + [cmd] self.eapi_positive_config_test(func, exp_cmds) # Test raising ValueError from invalid parameters cases = ['abc', 500, '101.101'] for bfd_ip in cases: func = function('set_bfd_ip', upd_intf, upd_vrid, value=bfd_ip) self.eapi_exception_config_test(func, ValueError) def test_vrconf_format(self): # Test the function to format a vrrp configuration to # match the output from get/getall vrconf = { 'priority': None, 'timers_advertise': None, 'mac_addr_adv_interval': None, 'preempt': 'default', 'preempt_delay_min': None, 'preempt_delay_reload': None, 'delay_reload': None, 'primary_ip': None, 'secondary_ip': ['10.10.4.22', '10.10.4.21'], 'description': None, 'enable': True, 'track': [ {'name': 'Ethernet1', 'action': 'shutdown'}, {'name': 'Ethernet1', 'action': 'decrement', 'amount': 10}, ], 'bfd_ip': None, 'ip_version': None} fixed = { 'priority': 100, 'timers_advertise': 1, 'mac_addr_adv_interval': 30, 'preempt': False, 'preempt_delay_min': 0, 'preempt_delay_reload': 0, 'delay_reload': 0, 'primary_ip': '0.0.0.0', 'secondary_ip': ['10.10.4.21', '10.10.4.22'], 'description': None, 'enable': True, 'track': [ {'name': 'Ethernet1', 'action': 'decrement', 'amount': 10}, {'name': 'Ethernet1', 'action': 'shutdown'}, ], 'bfd_ip': '', 'ip_version': 2} # Get the vrconf_format method from the library func = getattr(self.instance, 'vrconf_format') # Call the method with the vrconf dictionary result = func(vrconf) # And verify the result is a properly formatted dictionary self.assertEqual(fixed, result) if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_client.py0000644000076500000240000004630214447405743021005 0ustar dlyssenkostaff00000000000000# # Copyright (c) 2014, Arista Networks, Inc. # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of Arista Networks nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS # BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR # BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # import sys import os import unittest import imp sys.path.append(os.path.join(os.path.dirname(__file__), '../lib')) from unittest.mock import Mock, patch, call from testlib import get_fixture, random_string, random_int import pyeapi.client DEFAULT_CONFIG = {'connection:localhost': dict(transport='socket')} class TestNode(unittest.TestCase): def setUp(self): self.connection = Mock() self.node = pyeapi.client.Node(self.connection) def test_get_version_properties_match_version_number_no_match_model(self): self.node.enable = Mock() version = '4.17.1.1F-3512479.41711F (engineering build)' self.node.enable.return_value = [{'result': {'version': version, 'modelName': 'vEOS'}}] self.node._get_version_properties() self.assertEqual(self.node.version_number, '4.17.1.1') self.assertEqual(self.node.model, 'vEOS') def test_get_version_properties_no_match_version_number_match_model(self): self.node.enable = Mock() version = 'special-4.17.1.1F-3512479.41711F (engineering build)' model = 'DCS-7260QX-64-F' self.node.enable.return_value = [{'result': {'version': version, 'modelName': model}}] self.node._get_version_properties() self.assertEqual(self.node.version_number, version) self.assertEqual(self.node.model, '7260') def test_version_properties_populate(self): self.node.enable = Mock() version = '4.17.1.1F-3512479.41711F (engineering build)' self.node.enable.return_value = [{'result': {'version': version, 'modelName': 'vEOS'}}] self.node.version_number self.assertEqual(self.node.version_number, '4.17.1.1') self.assertEqual(self.node.version, version) self.assertEqual(self.node.model, 'vEOS') def test_enable_with_single_command(self): command = random_string() response = ['enable', command] self.connection.execute.return_value = {'result': list(response)} result = self.node.enable(command) self.connection.execute.assert_called_once_with(response, 'json') self.assertEqual(command, result[0]['result']) def test_enable_with_single_unicode_command(self): command = random_string() command = u'%s' % command response = ['enable', command] self.connection.execute.return_value = {'result': list(response)} result = self.node.enable(command) self.connection.execute.assert_called_once_with(response, 'json') self.assertEqual(command, result[0]['result']) def test_enable_with_single_extended_command(self): command = {'cmd': 'show cvx', 'revision': 2} response = ['enable', command] self.connection.execute.return_value = {'result': list(response)} result = self.node.enable(command) self.connection.execute.assert_called_once_with(response, 'json') self.assertEqual(command, result[0]['result']) def test_no_enable_with_single_command(self): command = random_string() response = [command] self.connection.execute.return_value = {'result': list(response)} result = self.node.enable(command, send_enable=False) self.connection.execute.assert_called_once_with(response, 'json') self.assertEqual(command, result[0]['result']) def test_enable_with_multiple_commands(self): commands = list() for i in range(0, random_int(2, 5)): commands.append(random_string()) def execute_response(cmds, *args): return {'result': [x for x in cmds]} self.connection.execute.side_effect = execute_response responses = self.node.enable(commands) self.assertEqual(self.connection.execute.call_count, len(commands)) expected_calls = [call(['enable', cmd], 'json') for cmd in commands] self.assertEqual(self.connection.execute.mock_calls, expected_calls) for index, response in enumerate(responses): self.assertEqual(commands[index], response['result']) def test_enable_with_multiple_unicode_commands(self): commands = list() for i in range(0, random_int(2, 5)): commands.append(u'%s' % random_string()) def execute_response(cmds, *args): return {'result': [x for x in cmds]} self.connection.execute.side_effect = execute_response responses = self.node.enable(commands) self.assertEqual(self.connection.execute.call_count, len(commands)) expected_calls = [call(['enable', cmd], 'json') for cmd in commands] self.assertEqual(self.connection.execute.mock_calls, expected_calls) for index, response in enumerate(responses): self.assertEqual(commands[index], response['result']) def test_config_with_single_command(self): command = random_string() self.node.run_commands = Mock(return_value=[{}, {}]) result = self.node.config(command) self.assertEqual(result, [{}]) def test_config_with_multiple_commands(self): commands = [random_string(), random_string()] self.node.run_commands = Mock(return_value=[{}, {}, {}]) result = self.node.config(commands) self.assertEqual(result, [{}, {}]) def test_config_with_single_multiline(self): command = ('banner login MULTILINE:This is a new banner\n' 'with different lines!!!') self.node.run_commands = Mock(return_value=[{}, {}]) result = self.node.config(command) self.assertEqual(result, [{}]) def test_config_with_multiple_multilines(self): commands = [random_string(), ('banner login MULTILINE:This is a new banner\n' 'with different lines!!!'), random_string()] self.node.run_commands = Mock(return_value=[{}, {}, {}, {}]) result = self.node.config(commands) self.assertEqual(result, [{}, {}, {}]) def test_get_config(self): config = [dict(output='test\nconfig')] self.node.run_commands = Mock(return_value=config) result = self.node.get_config() self.assertIsInstance(result, list) def test_get_config_as_string(self): config = [dict(output='test\nconfig')] self.node.run_commands = Mock(return_value=config) result = self.node.get_config(as_string=True) self.assertIsInstance(result, str) def test_get_config_raises_type_error(self): with self.assertRaises(TypeError): self.node.get_config('invalid-config') def test_api_autoloader(self): result = self.node.api('system') self.assertIsNotNone(result) def test_enable_authentication(self): self.assertIsNone(self.node._enablepwd) self.node.enable_authentication('test') self.assertEqual(self.node._enablepwd, 'test') def test_enable_with_config_statement(self): cmds = ['show version', 'configure', 'hostname foo'] with self.assertRaises(TypeError): self.node.enable(cmds) class TestClient(unittest.TestCase): def setUp(self): if 'EAPI_CONF' in os.environ: del os.environ['EAPI_CONF'] imp.reload(pyeapi.client) def test_load_config_for_connection_with_filename(self): conf = get_fixture('eapi.conf') pyeapi.client.load_config(filename=conf) cfg = pyeapi.client.config.get_connection('test1') self.assertEqual(cfg['host'], '192.168.1.16') self.assertEqual(cfg['username'], 'eapi') self.assertEqual(cfg['password'], 'password') self.assertEqual(cfg['enablepwd'], 'enablepwd') def test_load_config_for_connection_with_env(self): os.environ['EAPI_CONF'] = get_fixture('eapi.conf') pyeapi.client.load_config(random_string()) cfg = pyeapi.client.config.get_connection('test1') self.assertEqual(cfg['host'], '192.168.1.16') self.assertEqual(cfg['username'], 'eapi') self.assertEqual(cfg['password'], 'password') self.assertEqual(cfg['enablepwd'], 'enablepwd') def test_load_config(self): conf = get_fixture('eapi.conf') pyeapi.client.load_config(conf) self.assertEqual(len(pyeapi.client.config.sections()), 3) for name in ['localhost', 'test1', 'test2']: name = 'connection:%s' % name self.assertIn(name, pyeapi.client.config.sections()) def test_load_config_empty_conf(self): conf = get_fixture('empty.conf') pyeapi.client.load_config(filename=conf) conns = pyeapi.client.config.connections self.assertEqual(conns, ['localhost']) def test_load_config_yaml(self): conf = get_fixture('eapi.conf.yaml') pyeapi.client.load_config(filename=conf) conns = pyeapi.client.config.connections self.assertEqual(conns, ['localhost']) def test_load_config_env_path(self): os.environ['EAPI_CONF'] = get_fixture('env_path.conf') pyeapi.client.config.autoload() self.assertIn('connection:env_path', pyeapi.client.config.sections()) def test_config_always_has_default_connection(self): conf = '/invalid.conf' pyeapi.client.load_config(conf) self.assertEqual(len(pyeapi.client.config.sections()), 1) name = 'connection:localhost' self.assertIn(name, pyeapi.client.config.sections()) def test_connections_property(self): conf = get_fixture('eapi.conf') pyeapi.client.load_config(conf) connections = ['test1', 'test2', 'localhost'] result = pyeapi.client.config.connections self.assertEqual(sorted(connections), sorted(result)) def test_missing_connection_raises_attribute_error(self): with self.assertRaises(AttributeError): pyeapi.client.connect_to('invalid') def test_config_for_replaces_host_w_name(self): conf = get_fixture('nohost.conf') pyeapi.client.load_config(conf) cfg = pyeapi.config_for('test') self.assertEqual(cfg['host'], 'test') def test_hosts_for_tag_returns_none(self): result = pyeapi.client.hosts_for_tag(random_string()) self.assertIsNone(result) def test_hosts_for_tag_returns_names(self): conf = get_fixture('eapi.conf') pyeapi.client.load_config(conf) result = pyeapi.client.hosts_for_tag('tag1') self.assertEqual(sorted(['test1', 'test2']), sorted(result)) @patch('pyeapi.client.make_connection') def test_connect_types(self, connection): transports = list(pyeapi.client.TRANSPORTS.keys()) kwargs = dict(host='localhost', username='admin', password='', port=None, key_file=None, cert_file=None, ca_file=None, timeout=60, context=None) for transport in transports: pyeapi.client.connect(transport) connection.assert_called_with(transport, **kwargs) def test_make_connection_raises_typeerror(self): with self.assertRaises(TypeError): pyeapi.client.make_connection('invalid') def test_node_str_returns(self): node = pyeapi.client.Node(None) self.assertIsNotNone(str(node)) def test_node_repr_returns(self): node = pyeapi.client.Node(None) self.assertIsNotNone(repr(node)) def test_node_hasattr_connection(self): node = pyeapi.client.Node(None) self.assertTrue(hasattr(node, 'connection')) @patch('pyeapi.client.Node.get_config') def test_node_calls_running_config_with_all_by_default(self, get_config_mock): node = pyeapi.client.Node(None) _ = node.running_config get_config_mock.assert_called_once_with(params='all', as_string=True) @patch('pyeapi.client.Node.get_config') def test_node_calls_running_config_without_params_if_config_defaults_false( self, get_config_mock): node = pyeapi.client.Node(None, config_defaults=False) _ = node.running_config get_config_mock.assert_called_once_with(params=None, as_string=True) def test_node_returns_running_config(self): node = pyeapi.client.Node(None) get_config_mock = Mock(name='get_config') config_file = open(get_fixture('running_config.text')) config = config_file.read() config_file.close() get_config_mock.return_value = config node.get_config = get_config_mock self.assertIsInstance(node.running_config, str) def test_node_returns_startup_config(self): node = pyeapi.client.Node(None) get_config_mock = Mock(name='get_config') config_file = open(get_fixture('running_config.text')) config = config_file.read() config_file.close() get_config_mock.return_value = config node.get_config = get_config_mock self.assertIsInstance(node.startup_config, str) def test_node_returns_cached_startup_config(self): node = pyeapi.client.Node(None) config_file = open(get_fixture('running_config.text')) config = config_file.read() config_file.close() node._startup_config = config self.assertEqual(node.startup_config, config) def test_node_returns_version(self): node = pyeapi.client.Node(None) version = '4.17.1.1F-3512479.41711F (engineering build)' node.enable = Mock() node.enable.return_value = [{'result': {'version': version, 'modelName': 'vEOS'}}] self.assertIsInstance(node.version, str) self.assertEqual(node.version, version) def test_node_returns_cached_version(self): node = pyeapi.client.Node(None) node._version = '4.16.7R' self.assertEqual(node.version, '4.16.7R') def test_node_returns_version_number(self): node = pyeapi.client.Node(None) version = '4.17.1.1F-3512479.41711F (engineering build)' node.enable = Mock() node.enable.return_value = [{'result': {'version': version, 'modelName': 'vEOS'}}] self.assertIsInstance(node.version_number, str) self.assertIn(node.version_number, version) def test_node_returns_cached_version_number(self): node = pyeapi.client.Node(None) node._version_number = '4.16.7' self.assertEqual(node.version_number, '4.16.7') def test_node_returns_model(self): node = pyeapi.client.Node(None) version = '4.17.1.1F-3512479.41711F (engineering build)' model = 'DCS-7260QX-64-F' node.enable = Mock() node.enable.return_value = [{'result': {'version': version, 'modelName': model}}] self.assertIsInstance(node.model, str) self.assertIn(node.model, model) def test_node_returns_cached_model(self): node = pyeapi.client.Node(None) node._model = '7777' self.assertEqual(node.model, '7777') def test_connect_default_type(self): transport = Mock() with patch.dict(pyeapi.client.TRANSPORTS, {'https': transport}): pyeapi.client.connect() kwargs = dict(host='localhost', username='admin', password='', port=None, key_file=None, cert_file=None, ca_file=None, timeout=60, context=None) transport.assert_called_once_with(**kwargs) def test_connect_return_node(self): transport = Mock() with patch.dict(pyeapi.client.TRANSPORTS, {'https': transport}): conf = get_fixture('eapi.conf') pyeapi.client.load_config(filename=conf) node = pyeapi.client.connect(host='192.168.1.16', username='eapi', password='password', port=None, timeout=60, return_node=True) kwargs = dict(host='192.168.1.16', username='eapi', password='password', port=None, key_file=None, cert_file=None, ca_file=None, timeout=60, context=None) transport.assert_called_once_with(**kwargs) self.assertIsNone(node._enablepwd) def test_connect_return_node_enablepwd(self): transport = Mock() with patch.dict(pyeapi.client.TRANSPORTS, {'https': transport}): conf = get_fixture('eapi.conf') pyeapi.client.load_config(filename=conf) node = pyeapi.client.connect(host='192.168.1.16', username='eapi', password='password', port=None, timeout=60, enablepwd='enablepwd', return_node=True) kwargs = dict(host='192.168.1.16', username='eapi', password='password', port=None, key_file=None, cert_file=None, ca_file=None, timeout=60, context=None) transport.assert_called_once_with(**kwargs) self.assertEqual(node._enablepwd, 'enablepwd') def test_connect_to_with_config(self): transport = Mock() with patch.dict(pyeapi.client.TRANSPORTS, {'https': transport}): conf = get_fixture('eapi.conf') pyeapi.client.load_config(filename=conf) node = pyeapi.client.connect_to('test1') kwargs = dict(host='192.168.1.16', username='eapi', password='password', port=None, key_file=None, cert_file=None, ca_file=None, timeout=60, context=None) transport.assert_called_once_with(**kwargs) self.assertEqual(node._enablepwd, 'enablepwd') if __name__ == '__main__': unittest.main() pyeapi-1.0.2/test/unit/test_eapilib.py0000644000076500000240000002335014447405743021132 0ustar dlyssenkostaff00000000000000import unittest import json from unittest.mock import Mock, patch import pyeapi.eapilib class TestEapiConnection(unittest.TestCase): def test_execute_valid_response(self): response_dict = dict(jsonrpc='2.0', result=[], id=id(self)) mock_send = Mock(name='send') mock_send.return_value = json.dumps(response_dict) instance = pyeapi.eapilib.EapiConnection() instance.send = mock_send result = instance.execute(['command']) self.assertEqual(json.loads(result), response_dict) def test_execute_raises_type_error(self): instance = pyeapi.eapilib.EapiConnection() with self.assertRaises(TypeError): instance.execute(None, encoding='invalid') def test_execute_raises_connection_error(self): mock_send = Mock(name='send') mock_send.side_effect = pyeapi.eapilib.ConnectionError('test', 'test') instance = pyeapi.eapilib.EapiConnection() instance.send = mock_send with self.assertRaises(pyeapi.eapilib.ConnectionError): instance.execute('test') def test_execute_raises_command_error(self): mock_send = Mock(name='send') mock_send.side_effect = pyeapi.eapilib.CommandError('1000', 'test') instance = pyeapi.eapilib.EapiConnection() instance.send = mock_send with self.assertRaises(pyeapi.eapilib.CommandError): instance.execute('test') def test_create_socket_connection(self): instance = pyeapi.eapilib.SocketEapiConnection() self.assertIsInstance(instance, pyeapi.eapilib.EapiConnection) self.assertIsNotNone(str(instance.transport)) @patch('pyeapi.eapilib.socket') def test_socket_connection_create(self, mock_socket): instance = pyeapi.eapilib.SocketConnection('/path/to/sock') instance.connect() mock_socket.socket.return_value.connect.assert_called_with('/path/to/sock') def test_create_http_local_connection(self): instance = pyeapi.eapilib.HttpLocalEapiConnection() self.assertIsInstance(instance, pyeapi.eapilib.EapiConnection) self.assertIsNotNone(str(instance.transport)) def test_create_http_connection(self): instance = pyeapi.eapilib.HttpEapiConnection('localhost') self.assertIsInstance(instance, pyeapi.eapilib.EapiConnection) self.assertIsNotNone(str(instance.transport)) def test_create_https_connection(self): instance = pyeapi.eapilib.HttpsEapiConnection('localhost') self.assertIsInstance(instance, pyeapi.eapilib.EapiConnection) self.assertIsNotNone(str(instance.transport)) def test_send(self): response_dict = dict(jsonrpc='2.0', result=[{}], id=id(self)) response_json = json.dumps(response_dict) mock_transport = Mock(name='transport') mockcfg = {'getresponse.return_value.read.return_value': response_json} mock_transport.configure_mock(**mockcfg) instance = pyeapi.eapilib.EapiConnection() instance.transport = mock_transport instance.send('test') # HTTP requests to be processed by EAPI should always go to # the /command-api endpoint regardless of using TCP/IP or unix-socket # for the transport. Unix-socket implementation maps localhost to the # unix-socket - /var/run/command-api.sock mock_transport.putrequest.assert_called_once_with('POST', '/command-api') self.assertTrue(mock_transport.close.called) def test_send_with_authentication(self): response_dict = dict(jsonrpc='2.0', result=[{}], id=id(self)) response_json = json.dumps(response_dict) mock_transport = Mock(name='transport') mockcfg = {'getresponse.return_value.read.return_value': response_json} mock_transport.configure_mock(**mockcfg) instance = pyeapi.eapilib.EapiConnection() instance.authentication('username', 'password') instance.transport = mock_transport instance.send('test') self.assertTrue(mock_transport.close.called) def test_send_unauthorized_user(self): error_string = ('Unauthorized. Unable to authenticate user: Bad' ' username/password combination') response_str = ('Unable to authenticate user: Bad username/password' ' combination') mock_transport = Mock(name='transport') mockcfg = {'getresponse.return_value.read.return_value': response_str, 'getresponse.return_value.status': 401, 'getresponse.return_value.reason': 'Unauthorized'} mock_transport.configure_mock(**mockcfg) instance = pyeapi.eapilib.EapiConnection() instance.authentication('username', 'password') instance.transport = mock_transport try: instance.send('test') except pyeapi.eapilib.ConnectionError as err: self.assertEqual(err.message, error_string) def test_send_raises_connection_error(self): mock_transport = Mock(name='transport') mockcfg = {'getresponse.return_value.read.side_effect': ValueError} mock_transport.configure_mock(**mockcfg) instance = pyeapi.eapilib.EapiConnection() instance.transport = mock_transport try: instance.send('test') except pyeapi.eapilib.ConnectionError as err: self.assertEqual(err.message, 'unable to connect to eAPI') def test_send_raises_connection_socket_error(self): mock_transport = Mock(name='transport') mockcfg = {'getresponse.return_value.read.side_effect': OSError('timeout')} mock_transport.configure_mock(**mockcfg) instance = pyeapi.eapilib.EapiConnection() instance.transport = mock_transport try: instance.send('test') except pyeapi.eapilib.ConnectionError as err: error_msg = 'Socket error during eAPI connection: timeout' self.assertEqual(err.message, error_msg) def test_send_raises_command_error(self): error = dict(code=9999, message='test', data=[{'errors': ['test']}]) response_dict = dict(jsonrpc='2.0', error=error, id=id(self)) response_json = json.dumps(response_dict) mock_transport = Mock(name='transport') mockcfg = {'getresponse.return_value.read.return_value': response_json} mock_transport.configure_mock(**mockcfg) instance = pyeapi.eapilib.EapiConnection() instance.transport = mock_transport with self.assertRaises(pyeapi.eapilib.CommandError): instance.send('test') def test_send_raises_autocomplete_command_error(self): message = "runCmds() got an unexpected keyword argument 'autoComplete'" error = dict(code=9999, message=message, data=[{'errors': ['test']}]) response_dict = dict(jsonrpc='2.0', error=error, id=id(self)) response_json = json.dumps(response_dict) mock_transport = Mock(name='transport') mockcfg = {'getresponse.return_value.read.return_value': response_json} mock_transport.configure_mock(**mockcfg) instance = pyeapi.eapilib.EapiConnection() instance.transport = mock_transport try: instance.send('test') except pyeapi.eapilib.CommandError as error: match = ("autoComplete parameter is not supported in this version" " of EOS.") self.assertIn(match, error.message) def test_send_raises_expandaliases_command_error(self): message = "runCmds() got an unexpected keyword argument" \ " 'expandAliases'" error = dict(code=9999, message=message, data=[{'errors': ['test']}]) response_dict = dict(jsonrpc='2.0', error=error, id=id(self)) response_json = json.dumps(response_dict) mock_transport = Mock(name='transport') mockcfg = {'getresponse.return_value.read.return_value': response_json} mock_transport.configure_mock(**mockcfg) instance = pyeapi.eapilib.EapiConnection() instance.transport = mock_transport try: instance.send('test') except pyeapi.eapilib.CommandError as error: match = ("expandAliases parameter is not supported in this version" " of EOS.") self.assertIn(match, error.message) def test_request_adds_autocomplete(self): instance = pyeapi.eapilib.EapiConnection() request = instance.request(['sh ver'], encoding='json', autoComplete=True) data = json.loads(request) self.assertIn('autoComplete', data['params']) def test_request_adds_expandaliases(self): instance = pyeapi.eapilib.EapiConnection() request = instance.request(['test'], encoding='json', expandAliases=True) data = json.loads(request) self.assertIn('expandAliases', data['params']) def test_request_ignores_unknown_param(self): instance = pyeapi.eapilib.EapiConnection() request = instance.request(['sh ver'], encoding='json', unknown=True) data = json.loads(request) self.assertNotIn('unknown', data['params']) class TestCommandError(unittest.TestCase): def test_create_command_error(self): result = pyeapi.eapilib.CommandError(9999, 'test') self.assertIsInstance(result, pyeapi.eapilib.EapiError) def test_command_error_trace(self): commands = ['test command', 'test command', 'test command'] output = [{}, 'test output'] result = pyeapi.eapilib.CommandError(9999, 'test', commands=commands, output=output) self.assertIsNotNone(result.trace) pyeapi-1.0.2/test/unit/test_utils.py0000644000076500000240000000572614447405743020674 0ustar dlyssenkostaff00000000000000import unittest from unittest.mock import patch, Mock import pyeapi.utils from collections.abc import Iterable class TestUtils(unittest.TestCase): @patch('pyeapi.utils.import_module') def test_load_module(self, mock_import_module): loaded_module = Mock(object='loaded_module') mock_import_module.return_value = loaded_module result = pyeapi.utils.load_module('test') self.assertEqual(result, loaded_module) @patch('pyeapi.utils.import_module') def test_load_module_raises_import_error(self, mock_import_module): mock_import_module.return_value = None with self.assertRaises(ImportError): pyeapi.utils.load_module('test') def test_make_iterable_from_string(self): result = pyeapi.utils.make_iterable('test') self.assertIsInstance(result, Iterable) self.assertEqual(len(result), 1) def test_make_iterable_from_unicode(self): result = pyeapi.utils.make_iterable(u'test') self.assertIsInstance(result, Iterable) self.assertEqual(len(result), 1) def test_make_iterable_from_iterable(self): result = pyeapi.utils.make_iterable(['test']) self.assertIsInstance(result, Iterable) self.assertEqual(len(result), 1) def test_make_iterable_raises_type_error(self): with self.assertRaises(TypeError): pyeapi.utils.make_iterable(object()) def test_import_module(self): result = pyeapi.utils.import_module('pyeapi.api.vlans') self.assertIsNotNone(result) def test_import_module_raises_import_error(self): with self.assertRaises(ImportError): pyeapi.utils.import_module('fake.module.test') def test_expand_singles(self): vlans = '1,2,3' result = pyeapi.utils.expand_range(vlans) result = ','.join(result) self.assertTrue(vlans == result) def test_expand_range(self): vlans = '1-15' expected = [str(x) for x in range(1, 16)] result = pyeapi.utils.expand_range(vlans) self.assertEqual(result, expected) def test_expand_mixed(self): vlans = '1,3,5-7,9' result = pyeapi.utils.expand_range(vlans) self.assertEqual(result, ['1', '3', '5', '6', '7', '9']) def test_collapse_singles(self): vlans = '1,3,5,7' result = pyeapi.utils.collapse_range(vlans) self.assertEqual(result, ['1', '3', '5', '7']) def test_collapse_range(self): vlans = '1,2,3,4,5' result = pyeapi.utils.collapse_range(vlans) self.assertEqual(result, ['1-5']) def test_collapse_mixed(self): vlans = '1,3,5,6,7,9' result = pyeapi.utils.collapse_range(vlans) self.assertEqual(result, ['1', '3', '5-7', '9']) @patch('pyeapi.utils._LOGGER') def test_debug(self, mock_logger): pyeapi.utils.islocalconnection = Mock(return_value=True) pyeapi.utils.debug('test') mock_logger.debug.assert_called_with('test_utils.test_debug: test')