diff --git a/.gitignore b/.gitignore index e850f36..6e1c6fb 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ coverage.xml # Sphinx documentation docs/_build/ +docs/*_modules/ # PyBuilder target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f73b61..84fcc30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,66 +1,7 @@ Python Client for eAPI ====================== -## v0.3.2, 7/16/2015 +Full [release notes] [rns] hosted at readthedocs -- fixes a problem with parsing the hostname value in the system module -## v0.3.1, 6/14/2015 - -- 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 - -## v0.3.0, 5/4/2015 - -- 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 - -## v0.2.4, 4/30/2015 - -- adds required docs/description.rst for setup.py - -## v0.2.3, 4/29/2015 - -- fixes issue with importing syslog module on Windows - -## v0.2.2, 04/15/2015 - -- fixes an issue with eAPI error messages that do not return a data key - -## v0.2.1, 03/28/2015 - -- restores default certificate validation behavior for py2.7.9 - -## v0.2.0, 3/19/2015 - -- 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 - -## v0.1.1, 2/17/2015 - -- 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 - -## v0.1.0, 1/23/2015 - -- 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 +[rns]: http://pyeapi.readthedocs.org/en/master/release-notes.html diff --git a/Makefile b/Makefile index 6378f1d..4818b2d 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ VERSION := $(shell cat VERSION) ######################################################## -all: clean check pep8 pyflakes tests +all: clean check pep8 pyflakes tests pep8: -pep8 -r --ignore=E501,E221,W291,W391,E302,E251,E203,W293,E231,E303,E201,E225,E261,E241 pyeapi/ test/ @@ -62,3 +62,5 @@ unittest: clean systest: clean $(COVERAGE) run -m unittest discover test/system -v +coverage_report: + $(COVERAGE) report -m diff --git a/README.md b/README.md index c7de1d2..7d1e831 100644 --- a/README.md +++ b/README.md @@ -1,220 +1,43 @@ # 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/arista-eosplus/pyeapi/badge.svg?branch=develop)](https://coveralls.io/r/arista-eosplus/pyeapi?branch=develop) +[![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/arista-eosplus/pyeapi/badge.svg?branch=develop)](https://coveralls.io/r/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 properites. +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 convienent +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 implemenations. +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. -## Requirements - -* Arista EOS 4.12 or later -* Arista eAPI enabled for at least one transport (see Official EOS Config Guide - at arista.com for details) -* Python 2.7 / 3.4+ (Python 3 support is work in progress) - -# Getting Started -In order to use pyeapi, the EOS command API must be enabled using ``management -api http-commands`` configuration mode. This library supports eAPI calls over -both HTTP and UNIX Domain Sockets. Once the command API is enabled on the -destination node, create a configuration file with the node properities. - -**Note:** The default search path for the conf file is ``~/.eapi.conf`` -followed by ``/mnt/flash/eapi.conf``. This can be overridden by setting -``EAPI_CONF=`` in your environment. - -## Example eapi.conf File -Below is an example of an eAPI conf file. The conf file can contain more than -one node. Each node section must be prefaced by **connection:\** where -\ is the name of the connection. - -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. The - default value is _https_. Valid values are: - * socket (available in EOS 4.14.5 or later) - * http_local (available in EOS 4.14.5 or later) - * http - * https -* **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, deafult port: 443 - * transport: https_local, default port: 8080 - * transport: socket, default port: n/a - - -_Note:_ See the EOS User Manual found at arista.com for more details on -configuring eAPI values. - -All configuration values are optional. - -``` -[connection:veos01] -username: eapi -password: password -transport: http - -[connection:veos02] -transport: http - -[connection:veos03] -transport: socket - -[connection:veos04] -host: 172.16.10.1 -username: eapi -password: password -enablepwd: itsasecret -port: 1234 -transport: https - -[connection:localhost] -transport: http_local -``` - -The above example shows different ways to define EOS node connections. All -configuration options will attempt to use default values if not explicitly -defined. If the host parameter is not set for a given entry, then the -connection name will be used as the host address. - -### Configuring \[connection:localhost] - -The pyeapi library automatically installs a single default configuration entry -for connecting to localhost host using a transport of sockets. If using the -pyeapi library locally on an EOS node, simply enable the command API to use -sockets and no further configuration is needed for pyeapi to function. If you -specify an entry in a conf file with the name ``[connection:localhost]``, the -values in the conf file will overwrite the default. - -## Using pyeapi -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. - -### Creating a connection and sending commands -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. - -``` -# 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', 'result': {u'hostname': u'veos01', u'fqdn': -u'veos01.arista.com'}, 'encoding': 'json'}] - -# 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 -``` - -### Using the API - -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 - -``` -# 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') +## Documentation -# 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 -``` - -All API implementations developed by Arista EOS+ CS are found in the pyeapi/api -folder. See the examples folder for additional examples. - -# Installation - -The source code for pyeapi is provided on Github at -http://github.com/arista-eosplus/pyeapi. All current development is done in -the develop branch. Stable released versions are tagged in the master branch -and uploaded to PyPi. +* [Quickstart] [quickstart] +* [Installation] [install] +* [Modules] [modules] +* [Release Notes] [rns] +* [Contribute] [contribute] -* To install the latest stable version of pyeapi, simply run ``pip install - pyeapi`` (or ``pip install --upgrade pyeapi``) -* To install the latest development version from Github, simply clone the - develop branch and run ``python setup.py install`` - -# 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. +### Building Local Documentation -* To run the unit tests, simply run ``make unittest`` from the root of the - pyeapi source folder -* To run the system tests, simply run ``make systest`` from the root of the - pyeapi source fodler -* To run all tests, use ``make tests`` from the root of the pyeapi source - folder +If you cannot access readthedocs.org you have the option of building the +documentation locally. - -# Contributing - -Contributing pull requests are gladly welcomed for this repository. Please -note that all contributions that modify the library behavior require -corresponding test cases otherwise the pull request will be rejected. +1. ``pip install -r dev-requirements.txt`` +2. ``cd docs`` +3. ``make html`` +4. ``open _build/html/index.html`` # License -Copyright (c) 2014, Arista Networks EOS+ +Copyright (c) 2015, Arista Networks EOS+ All rights reserved. Redistribution and use in source and binary forms, with or without @@ -242,3 +65,11 @@ 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 diff --git a/VERSION b/VERSION index 1c09c74..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.3 +0.4.0 diff --git a/docs/Makefile b/docs/Makefile index d1d0fa5..da55edf 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -6,6 +6,9 @@ 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) @@ -48,12 +51,19 @@ help: 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 diff --git a/docs/conf.py b/docs/conf.py old mode 100644 new mode 100755 index 205c966..c0540be --- a/docs/conf.py +++ b/docs/conf.py @@ -33,6 +33,8 @@ 'sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', + 'sphinx.ext.doctest', + 'sphinxcontrib.napoleon' ] # Add any paths that contain templates here, relative to this directory. diff --git a/docs/configfile.rst b/docs/configfile.rst new file mode 100644 index 0000000..b27ce5e --- /dev/null +++ b/docs/configfile.rst @@ -0,0 +1,193 @@ +.. _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 + +: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, deafult 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 +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 + +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 + +******************* +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 diff --git a/docs/contribute.rst b/docs/contribute.rst new file mode 100644 index 0000000..b94866f --- /dev/null +++ b/docs/contribute.rst @@ -0,0 +1,43 @@ + +########## +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. diff --git a/docs/description.rst b/docs/description.rst index 454701c..01d2657 100644 --- a/docs/description.rst +++ b/docs/description.rst @@ -1,5 +1,5 @@ -Python Client for eAPI -====================== +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 @@ -10,9 +10,9 @@ The Python library can be used to communicate with EOS either locally 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 convienent +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 implemenations. +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 diff --git a/docs/generate_modules.py b/docs/generate_modules.py new file mode 100755 index 0000000..a431e34 --- /dev/null +++ b/docs/generate_modules.py @@ -0,0 +1,95 @@ +#!/usr/bin/python + +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('__')] + 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() diff --git a/docs/index.rst b/docs/index.rst index df6420a..f2aeb5c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,27 +1,44 @@ -.. Python Client for eAPI documentation master file, created by - sphinx-quickstart on Tue Mar 24 21:01:31 2015. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +.. _intro: -Welcome to Python Client for eAPI's documentation! -================================================== +############ +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. -Contents: .. toctree:: - :maxdepth: 2 + :maxdepth: 1 - pyeapi - pyeapi.api + install + quickstart + configfile modules + requirements + contribute + release-notes + support license - +****************** Indices and tables -================== +****************** * :ref:`genindex` * :ref:`modindex` -* :ref:`search` - diff --git a/docs/install.rst b/docs/install.rst new file mode 100644 index 0000000..f40a867 --- /dev/null +++ b/docs/install.rst @@ -0,0 +1,136 @@ +.. _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:: console + + # 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:: console + + # 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:: console + + # 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 + +.. 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:: console + + 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. diff --git a/docs/modules.rst b/docs/modules.rst index 4e8c689..edac209 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -1,7 +1,8 @@ -pyeapi -====== +Modules +======= .. toctree:: :maxdepth: 4 - pyeapi + client_modules/_list_of_modules + api_modules/_list_of_modules diff --git a/docs/pyeapi.api.rst b/docs/pyeapi.api.rst deleted file mode 100644 index 55566e3..0000000 --- a/docs/pyeapi.api.rst +++ /dev/null @@ -1,70 +0,0 @@ -pyeapi.api package -================== - -Submodules ----------- - -pyeapi.api.abstract module --------------------------- - -.. automodule:: pyeapi.api.abstract - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.interfaces module ----------------------------- - -.. automodule:: pyeapi.api.interfaces - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.ipinterfaces module ------------------------------- - -.. automodule:: pyeapi.api.ipinterfaces - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.spanningtree module ------------------------------- - -.. automodule:: pyeapi.api.spanningtree - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.stp module ---------------------- - -.. automodule:: pyeapi.api.stp - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.switchports module ------------------------------ - -.. automodule:: pyeapi.api.switchports - :members: - :undoc-members: - :show-inheritance: - -pyeapi.api.vlans module ------------------------ - -.. automodule:: pyeapi.api.vlans - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: pyeapi.api - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/pyeapi.rst b/docs/pyeapi.rst deleted file mode 100644 index 958d2a2..0000000 --- a/docs/pyeapi.rst +++ /dev/null @@ -1,45 +0,0 @@ -pyeapi package -============== - -Subpackages ------------ - -.. toctree:: - - pyeapi.api - -Submodules ----------- - -pyeapi.client module --------------------- - -.. automodule:: pyeapi.client - :members: - :undoc-members: - :show-inheritance: - -pyeapi.eapilib module ---------------------- - -.. automodule:: pyeapi.eapilib - :members: - :undoc-members: - :show-inheritance: - -pyeapi.utils module -------------------- - -.. automodule:: pyeapi.utils - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: pyeapi - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 79aed30..bcd2d09 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -1,217 +1,114 @@ -Python Client for eAPI -====================== - -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 properites. - -The pyeapi library also provides an API layer for building native Python -objects to interact with the destination nodes. The API layer is a convienent -implementation for working with the EOS configuration and is extensible for -developing custom implemenations. - -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. - -## Requirements - -* Arista EOS 4.12 or later -* Arista eAPI enabled for at least one transport (see Official EOS Config Guide - at arista.com for details) -* Python 2.7 - -# Getting Started -In order to use pyeapi, the EOS command API must be enabled using ``management -api http-commands`` configuration mode. This library supports eAPI calls over -both HTTP and UNIX Domain Sockets. Once the command API is enabled on the -destination node, create a configuration file with the node properities. - -**Note:** The default search path for the conf file is ``~/.eapi.conf`` -followed by ``/mnt/flash/eapi.conf``. This can be overridden by setting -``EAPI_CONF=`` in your environment. - -## Example eapi.conf File -Below is an example of an eAPI conf file. The conf file can contain more than -one node. Each node section must be prefaced by **connection:\** where -\ is the name of the connection. - -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. The - default value is _https_. Valid values are: - * socket (available in EOS 4.14.5 or later) - * http_local (available in EOS 4.14.5 or later) - * http - * https -* **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, deafult port: 443 - * transport: https_local, default port: 8080 - * transport: socket, default port: n/a - - -_Note:_ See the EOS User Manual found at arista.com for more details on -configuring eAPI values. - -All configuration values are optional. - -``` -[connection:veos01] -username: eapi -password: password -transport: http - -[connection:veos02] -transport: http - -[connection:veos03] -transport: socket - -[connection:veos04] -host: 172.16.10.1 -username: eapi -password: password -enablepwd: itsasecret -port: 1234 -transport: https - -[connection:localhost] -transport: http_local -``` - -The above example shows different ways to define EOS node connections. All -configuration options will attempt to use default values if not explicitly -defined. If the host parameter is not set for a given entry, then the -connection name will be used as the host address. - -### Configuring \[connection:localhost] - -The pyeapi library automatically installs a single default configuration entry -for connecting to localhost host using a transport of sockets. If using the -pyeapi library locally on an EOS node, simply enable the command API to use -sockets and no further configuration is needed for pyeapi to function. If you -specify an entry in a conf file with the name ``[connection:localhost]``, the -values in the conf file will overwrite the default. - -## Using pyeapi +########## +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 + - 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. +plane. -### Creating a connection and sending commands 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. +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. -``` -# start by importing the library -import pyeapi +.. code-block:: python -# create a node object by specifying the node to work with -node = pyeapi.connect_to('veos01') + # start by importing the library + import pyeapi -# send one or more commands to the node -node.enable('show hostname') -[{'command': 'show hostname', 'result': {u'hostname': u'veos01', u'fqdn': -u'veos01.arista.com'}, 'encoding': 'json'}] + # create a node object by specifying the node to work with + node = pyeapi.connect_to('veos01') -# use the config method to send configuration commands -node.config('hostname veos01') -[{}] + # send one or more commands to the node + node.enable('show hostname') + [{'command': 'show hostname', 'result': {u'hostname': u'veos01', u'fqdn': + u'veos01.arista.com'}, 'encoding': 'json'}] -# multiple commands can be sent by using a list (works for both enable or -config) -node.config(['interface Ethernet1', 'description foo']) -[{}, {}] + # use the config method to send configuration commands + node.config('hostname veos01') + [{}] -# return the running or startup configuration from the node (output omitted for -brevity) -node.running_config + # multiple commands can be sent by using a list + # (works for both enable or config) + node.config(['interface Ethernet1', 'description foo']) + [{}, {}] -node.startup_config -``` + # return the running or startup configuration from the + # node (output omitted for brevity) + node.running_config -### Using the API + 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 -``` -# 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 -``` - -All API implementations developed by Arista EOS+ CS are found in the pyeapi/api -folder. See the examples folder for additional examples. - -# Installation - -The source code for pyeapi is provided on Github at -http://github.com/arista-eosplus/pyeapi. All current development is done in -the develop branch. Stable released versions are tagged in the master branch -and uploaded to PyPi. - -* To install the latest stable version of pyeapi, simply run ``pip install - pyeapi`` (or ``pip install --upgrade pyeapi``) -* To install the latest development version from Github, simply clone the - develop branch and run ``python setup.py install`` - -# 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. - -* To run the unit tests, simply run ``make unittest`` from the root of the - pyeapi source folder -* To run the system tests, simply run ``make systest`` from the root of the - pyeapi source fodler -* To run all tests, use ``make tests`` from the root of the pyeapi source - folder +.. code-block:: python + # create a connection to the node + import pyeapi + node = pyeapi.connect_to('veos01') -# Contributing + # get the instance of the API (in this case vlans) + vlans = node.api('vlans') -Contributing pull requests are gladly welcomed for this repository. Please -note that all contributions that modify the library behavior require -corresponding test cases otherwise the pull request will be rejected. + # 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': + []}} -# License + # return a specific vlan from the node + vlans.get(1) + {'state': 'active', 'name': 'default', 'vlan_id': 1, 'trunk_groups': []} -New BSD, See [LICENSE](LICENSE) file + # add a new vlan to the node + vlans.create(100) + True + # set the new vlan name + vlans.set_name(100, 'foo') + True diff --git a/docs/release-notes-0.1.0.rst b/docs/release-notes-0.1.0.rst new file mode 100644 index 0000000..1dfb56f --- /dev/null +++ b/docs/release-notes-0.1.0.rst @@ -0,0 +1,12 @@ +###### +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 diff --git a/docs/release-notes-0.1.1.rst b/docs/release-notes-0.1.1.rst new file mode 100644 index 0000000..bd50d6c --- /dev/null +++ b/docs/release-notes-0.1.1.rst @@ -0,0 +1,12 @@ +###### +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 diff --git a/docs/release-notes-0.2.0.rst b/docs/release-notes-0.2.0.rst new file mode 100644 index 0000000..0ca95fd --- /dev/null +++ b/docs/release-notes-0.2.0.rst @@ -0,0 +1,13 @@ +###### +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 diff --git a/docs/release-notes-0.2.1.rst b/docs/release-notes-0.2.1.rst new file mode 100644 index 0000000..8f4a340 --- /dev/null +++ b/docs/release-notes-0.2.1.rst @@ -0,0 +1,7 @@ +###### +v0.2.1 +###### + +2015-03-28 + +- restores default certificate validation behavior for py2.7.9 diff --git a/docs/release-notes-0.2.2.rst b/docs/release-notes-0.2.2.rst new file mode 100644 index 0000000..b23a08c --- /dev/null +++ b/docs/release-notes-0.2.2.rst @@ -0,0 +1,7 @@ +###### +v0.2.2 +###### + +2015-04-15 + +- fixes an issue with eAPI error messages that do not return a data key diff --git a/docs/release-notes-0.2.3.rst b/docs/release-notes-0.2.3.rst new file mode 100644 index 0000000..565b5a3 --- /dev/null +++ b/docs/release-notes-0.2.3.rst @@ -0,0 +1,7 @@ +###### +v0.2.3 +###### + +2015-04-29 + +- fixes issue with importing syslog module on Windows diff --git a/docs/release-notes-0.2.4.rst b/docs/release-notes-0.2.4.rst new file mode 100644 index 0000000..efd8ec5 --- /dev/null +++ b/docs/release-notes-0.2.4.rst @@ -0,0 +1,7 @@ +###### +v0.2.4 +###### + +2015-04-30 + +- adds required docs/description.rst for setup.py diff --git a/docs/release-notes-0.3.0.rst b/docs/release-notes-0.3.0.rst new file mode 100644 index 0000000..03d74aa --- /dev/null +++ b/docs/release-notes-0.3.0.rst @@ -0,0 +1,13 @@ +###### +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 diff --git a/docs/release-notes-0.3.1.rst b/docs/release-notes-0.3.1.rst new file mode 100644 index 0000000..8101cbc --- /dev/null +++ b/docs/release-notes-0.3.1.rst @@ -0,0 +1,9 @@ +###### +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 diff --git a/docs/release-notes-0.3.2.rst b/docs/release-notes-0.3.2.rst new file mode 100644 index 0000000..621d255 --- /dev/null +++ b/docs/release-notes-0.3.2.rst @@ -0,0 +1,7 @@ +###### +v0.3.2 +###### + +2015-07-16 + +- fixes a problem with parsing the hostname value in the system module diff --git a/docs/release-notes-0.3.3.rst b/docs/release-notes-0.3.3.rst new file mode 100644 index 0000000..93d034c --- /dev/null +++ b/docs/release-notes-0.3.3.rst @@ -0,0 +1,9 @@ +###### +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 diff --git a/docs/release-notes-0.4.0.rst b/docs/release-notes-0.4.0.rst new file mode 100644 index 0000000..40b3475 --- /dev/null +++ b/docs/release-notes-0.4.0.rst @@ -0,0 +1,53 @@ +###### +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 diff --git a/docs/release-notes.rst b/docs/release-notes.rst new file mode 100644 index 0000000..a4591ec --- /dev/null +++ b/docs/release-notes.rst @@ -0,0 +1,20 @@ +############# +Release Notes +############# + +.. toctree:: + :maxdepth: 2 + :titlesonly: + + release-notes-0.1.0.rst + release-notes-0.1.1.rst + release-notes-0.2.0.rst + release-notes-0.2.1.rst + release-notes-0.2.2.rst + release-notes-0.2.3.rst + release-notes-0.2.4.rst + release-notes-0.3.0.rst + release-notes-0.3.1.rst + release-notes-0.3.2.rst + release-notes-0.3.3.rst + release-notes-0.4.0.rst diff --git a/docs/requirements.rst b/docs/requirements.rst new file mode 100644 index 0000000..fe7cb19 --- /dev/null +++ b/docs/requirements.rst @@ -0,0 +1,11 @@ +############ +Requirements +############ + +* Arista EOS 4.12 or later +* Arista eAPI enabled for at least one transport (see Official EOS Config Guide + at arista.com for details) +* Python 2.7 or 3.4+ (Python 3 support is work in progress) +* Pyeapi requires the netaddr Python module + +.. Note:: netaddr gets installed automatically if you use pip to install pyeapi diff --git a/docs/support.rst b/docs/support.rst new file mode 100644 index 0000000..282f366 --- /dev/null +++ b/docs/support.rst @@ -0,0 +1,33 @@ +####### +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. diff --git a/pyeapi/__init__.py b/pyeapi/__init__.py index f71ea74..4863f17 100644 --- a/pyeapi/__init__.py +++ b/pyeapi/__init__.py @@ -29,7 +29,7 @@ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -__version__ = '0.3.3' +__version__ = '0.4.0' __author__ = 'Arista EOS+' diff --git a/pyeapi/api/bgp.py b/pyeapi/api/bgp.py index 975e18e..810300e 100644 --- a/pyeapi/api/bgp.py +++ b/pyeapi/api/bgp.py @@ -69,6 +69,7 @@ def get(self): 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)) @@ -85,6 +86,12 @@ def _parse_router_id(self, 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) @@ -129,6 +136,20 @@ def set_router_id(self, value=None, default=False): cmd = self.command_builder('router-id', value=value, default=default) return self.configure_bgp(cmd) + def set_maximum_paths(self, max_path=None, max_ecmp_path=None, default=False): + if not max_path and max_ecmp_path: + raise TypeError('Cannot use maximum_ecmp_paths without ' + 'providing max_path') + if default: + cmd = 'default maximum-paths' + elif max_path: + cmd = 'maximum-paths {}'.format(max_path) + if max_ecmp_path: + cmd += ' ecmp {}'.format(max_ecmp_path) + else: + cmd = 'no maximum-paths' + return self.configure_bgp(cmd) + def set_shutdown(self, value=None, default=False): cmd = self.command_builder('shutdown', value=value, default=default) return self.configure_bgp(cmd) @@ -189,9 +210,10 @@ def _parse_send_community(self, config, name): return dict(send_community=not value) def _parse_shutdown(self, config, name): - exp = 'no neighbor {} shutdown'.format(name) - value = exp in config - return dict(shutdown=not value) + regexp = r'(?, - "type": "ethernet", - "sflow": [true, false], - "flowcontrol_send": [on, off], - "flowcontrol_receive": [on, off] - } - Args: name (string): the interface identifier to retrieve the from the configuration @@ -320,7 +310,15 @@ def get(self, name): 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. + 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) @@ -334,7 +332,6 @@ def get(self, name): resource.update(self._parse_flowcontrol_receive(config)) return resource - def _parse_sflow(self, config): """Scans the specified config block and returns the sflow value @@ -383,7 +380,6 @@ def _parse_flowcontrol_receive(self, config): value = match.group(1) return dict(flowcontrol_receive=value) - def create(self, name): """Creating Ethernet interfaces is currently not supported @@ -512,23 +508,23 @@ def __str__(self): def get(self, name): """Returns a Port-Channel interface as a set of key/value pairs - Example: - { - "name": , - "type": "portchannel", - "members": , - "minimum_links: , - "lacp_mode": [on, active, passive] - } - 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 + 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: @@ -549,7 +545,6 @@ def _parse_minimum_links(self, config): value = int(match.group(1)) return dict(minimum_links=value) - def get_lacp_mode(self, name): """Returns the LACP mode for the specified Port-Channel interface @@ -571,8 +566,6 @@ def get_lacp_mode(self, name): self.get_block('^interface %s' % member)) return match.group('value') - - def get_members(self, name): """Returns the member interfaces for the specified Port-Channel @@ -587,9 +580,10 @@ def get_members(self, name): 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'Ethernet[\d/]*', config[0]['result']['output']) + return re.findall(r'\b(?!Peer)Ethernet[\d/]*\b', + config[0]['result']['output']) - def set_members(self, name, members): + def set_members(self, name, members, mode=None): """Configures the array of member interfaces for the Port-Channel Args: @@ -599,14 +593,24 @@ def set_members(self, name, members): 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) - grpid = re.search(r'(\d+)', name).group() - - commands = list() + 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): @@ -917,7 +921,3 @@ def remove_vlan(self, name, vid): def instance(api): return Interfaces(api) - - - - diff --git a/pyeapi/api/ipinterfaces.py b/pyeapi/api/ipinterfaces.py index d53a359..9b8d750 100644 --- a/pyeapi/api/ipinterfaces.py +++ b/pyeapi/api/ipinterfaces.py @@ -120,16 +120,15 @@ def _parse_mtu(self, config): def getall(self): """ Returns all of the IP interfaces found in the running-config - Example: - { - 'Ethernet1': {...}, - 'Ethernet2': {...} - } - Returns: A Python dictionary object of key/value pairs keyed by interface - name that represents all of the IP interfaces on - the current node. + name that represents all of the IP interfaces on + the current node:: + + { + 'Ethernet1': {...}, + 'Ethernet2': {...} + } """ interfaces_re = re.compile(r'^interface\s(.+)', re.M) diff --git a/pyeapi/api/mlag.py b/pyeapi/api/mlag.py index 71a92ff..775821b 100644 --- a/pyeapi/api/mlag.py +++ b/pyeapi/api/mlag.py @@ -158,7 +158,7 @@ def _parse_peer_link(self, config): dict: A dict object that is intended to be merged into the resource dict """ - match = re.search(r'peer-link (\w+)', config) + match = re.search(r'peer-link (\S+)', config) value = match.group(1) if match else None return dict(peer_link=value) @@ -289,4 +289,3 @@ def instance(node): object: An instance of Mlag """ return Mlag(node) - diff --git a/pyeapi/api/routemaps.py b/pyeapi/api/routemaps.py new file mode 100644 index 0000000..b1b10d7 --- /dev/null +++ b/pyeapi/api/routemaps.py @@ -0,0 +1,354 @@ +# +# 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 Entity, 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: + 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: + 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): + """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 + + 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 value is not None: + if value < 1: + raise ValueError('seqno must be a positive integer') + commands.append('continue %s' % value) + else: + commands.append('no continue') + + return self.configure(commands) + + def set_description(self, name, action, seqno, value=None, default=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 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 description') + elif value is not None: + commands.append('no description') + commands.append('description %s' % value) + else: + commands.append('no description') + + 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) diff --git a/pyeapi/api/spanningtree.py b/pyeapi/api/spanningtree.py index be66be1..8d3eb5b 100644 --- a/pyeapi/api/spanningtree.py +++ b/pyeapi/api/spanningtree.py @@ -31,7 +31,7 @@ # import warnings -warnings.warn("Api module spanningtree is dereciated. Please update api " +warnings.warn("Api module spanningtree is depricated. Please update api " "calls to use stp instead") from pyeapi.api.stp import instance # flake8: noqa diff --git a/pyeapi/api/staticroute.py b/pyeapi/api/staticroute.py new file mode 100644 index 0000000..6f43ae0 --- /dev/null +++ b/pyeapi/api/staticroute.py @@ -0,0 +1,392 @@ +# +# 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] is '' else match[2] + distance = int(match[3]) + + # Create the data dict with the remaining components + data = {} + data['tag'] = None if match[4] is '' else int(match[4]) + data['route_name'] = None if match[5] is '' 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 (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + 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 (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + 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 (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + 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 (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + 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 (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + 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, next_hop_ip=None, + # distance=None, tag=None, route_name=None): + 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 (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + 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, next_hop_ip=None, + # distance=None, tag=None, route_name=None, + # delete=False, default=False): + 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 (dict): A key/value dictionary containing + next_hop_ip (string): The next hop address on destination + interface + distance (string): Administrative distance for this route + tag (string): Route tag + route_name (string): Route name + delete (boolean): If true, deletes the specified route + instead of creating or setting values for the route + 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. + """ + + # Build the route string based on the parameters given + # commands = self._build_commands(ip_dest, next_hop, + # next_hop_ip=next_hop_ip, + # distance=distance, + # tag=tag, + # route_name=route_name) + 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) diff --git a/pyeapi/api/stp.py b/pyeapi/api/stp.py index 88b2755..03db8fa 100644 --- a/pyeapi/api/stp.py +++ b/pyeapi/api/stp.py @@ -98,20 +98,18 @@ def get(self): interfaces and instances. See the StpInterfaces and StpInstances classes for the key/value pair definitions. - Example - { - "mode": [mstp, none], - "interfaces": {...}, - "instances": {...} - } - 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 + the entire supported spanning-tree configuration:: + { + "mode": [mstp, none], + "interfaces": {...}, + "instances": {...} + } """ return dict(interfaces=self.interfaces.getall(), instances=self.instances.getall()) diff --git a/pyeapi/api/users.py b/pyeapi/api/users.py index ae9e67f..df5e67b 100644 --- a/pyeapi/api/users.py +++ b/pyeapi/api/users.py @@ -31,7 +31,7 @@ # """API Module for working with EOS local user resources -The Users resource provides configuraiton of local user resources for +The Users resource provides configuration of local user resources for an EOS node. Parameters: @@ -74,13 +74,18 @@ def isprivilege(value): class Users(EntityCollection): - """The Users class provides a configuration resource for local users + """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. """ - users_re = re.compile(r'username ([^\s]+) privilege (\d+)' + users_re = re.compile(r'username (?P[^\s]+) privilege (\d+)' r'(?: role ([^\s]+))?' r'(?: (nopassword))?' - r'(?: secret ([0,5,7]) (.+))?', re.M) + r'(?: secret (0|5|7|sha512) (.+))?' + r'.*$\n(?:username (?P=user) sshkey (.+)$)?', re.M) def get(self, name): """Returns the local user configuration as a resource dict @@ -97,14 +102,10 @@ def get(self, name): return self.getall().get(name) def getall(self): - """Returns the local user configuration as a resource dict - - Args: - name (str): The username to return from the nodes global running- - config. + """Returns all local users configuration as a resource dict Returns: - dict: A resource dict object + dict: A dict of usernames with a nested resource dict object """ users = self.users_re.findall(self.config, re.M) resources = dict() @@ -122,19 +123,20 @@ def _parse_username(self, config): dict: A resource dict that is intended to be merged into the user resource """ - (username, priv, role, nopass, fmt, secret) = config + (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 + 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 require either a secret (password) or the nopassword + Creating users requires either a secret (password) or the nopassword keyword to be specified. Args: @@ -243,14 +245,14 @@ def set_privilege(self, name, value=None): raise TypeError('priviledge value must be between 0 and 15') cmd += ' privilege %s' % value else: - cmd += ' no privilege' + cmd += ' privilege 1' return self.configure(cmd) def set_role(self, name, value=None): """Configures the user role vale in EOS Args: - name (str): The name of the user to craete + name (str): The name of the user to create value (str): The value to configure for the user role @@ -261,9 +263,26 @@ def set_role(self, name, value=None): if value is not None: cmd += ' role %s' % value else: - cmd += ' no role' + cmd = 'default username %s role' % name return self.configure(cmd) + def set_sshkey(self, name, value=None): + """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. + + Returns: + True if the operation was successful otherwise False + """ + cmd = 'username %s' % name + if value: + cmd += ' sshkey %s' % value + else: + cmd = 'no username %s sshkey' % name + return self.configure(cmd) def instance(node): """Returns an instance of Users @@ -277,4 +296,3 @@ def instance(node): resource """ return Users(node) - diff --git a/pyeapi/api/varp.py b/pyeapi/api/varp.py new file mode 100644 index 0000000..b3ef80f --- /dev/null +++ b/pyeapi/api/varp.py @@ -0,0 +1,202 @@ +# +# 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): + """ 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). + + Returns: + True if the set operation succeeds otherwise False. + """ + if default: + commands = 'default ip virtual-router mac-address' + elif 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') + commands = 'ip virtual-router mac-address %s' % mac_address + else: + commands = 'no ip virtual-router mac-address' + + 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): + + commands = list() + commands.append('interface %s' % name) + + if default: + commands.append('default ip virtual-router address') + elif addresses is not None: + try: + current_addresses = self.get(name)['addresses'] + except: + 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) diff --git a/pyeapi/api/vrrp.py b/pyeapi/api/vrrp.py new file mode 100644 index 0000000..e9dcc07 --- /dev/null +++ b/pyeapi/api/vrrp.py @@ -0,0 +1,1379 @@ +# +# 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): + 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): + 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): + 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): + 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): + 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): + 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): + 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): + 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): + 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: + True if the command executed successfully, error if failure + If run is False: + The formatted command string which can be passed to the node + + """ + + if value is False: + cmd = "vrrp %d shutdown" % vrid + elif value is True: + 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: + True if the command executed successfully, error if failure. + If run is False: + 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'] + 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'] + cmd = "no vrrp %d ip %s" % (vrid, primary_ip) + elif re.match(r'^\d+\.\d+\.\d+\.\d+$', str(value)): + 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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d priority" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d priority" % vrid + elif re.match(r'^\d+$', str(value)) and 1 <= value <= 254: + cmd = "vrrp %d priority %s" % (vrid, value) + else: + raise ValueError("vrrp property 'priority' must be " + "an integer in the range 1-254") + + # 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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d description" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d description" % vrid + else: + cmd = "vrrp %d description %s" % (vrid, value) + + # 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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d ip version" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d ip version" % vrid + elif value in (2, 3): + cmd = "vrrp %d ip version %d" % (vrid, value) + else: + raise ValueError("vrrp property 'ip_version' must be 2 or 3") + + # 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: + True if the command executed successfully, error if failure. + If run is False: + 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: + cmds.append("no vrrp %d ip %s secondary" % (vrid, sec_ip)) + + for sec_ip in add: + 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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d timers advertise" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d timers advertise" % vrid + elif int(value) and 1 <= int(value) <= 255: + cmd = "vrrp %d timers advertise %d" % (vrid, value) + else: + raise ValueError("vrrp property 'timers_advertise' must be" + "in the range 1-255") + + # 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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d mac-address advertisement-interval" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d mac-address advertisement-interval" % vrid + elif int(value) and 1 <= int(value) <= 3600: + cmd = "vrrp %d mac-address advertisement-interval %d" \ + % (vrid, value) + else: + raise ValueError("vrrp property 'mac_addr_adv_interval' must be" + "in the range 1-3600") + + # 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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d preempt" % vrid + elif disable is True or value is None or value is False: + cmd = "no vrrp %d preempt" % vrid + elif value is True: + cmd = "vrrp %d preempt" % vrid + else: + raise ValueError("vrrp property 'preempt' 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_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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d preempt delay minimum" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d preempt delay minimum" % vrid + elif int(value) and 0 <= int(value) <= 3600: + cmd = "vrrp %d preempt delay minimum %d" % (vrid, value) + else: + raise ValueError("vrrp property 'preempt_delay_min' must be" + "in the range 0-3600 %r" % value) + + # 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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d preempt delay reload" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d preempt delay reload" % vrid + elif int(value) and 0 <= int(value) <= 3600: + cmd = "vrrp %d preempt delay reload %d" % (vrid, value) + else: + raise ValueError("vrrp property 'preempt_delay_reload' must be" + "in the range 0-3600 %r" % value) + + # 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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d delay reload" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d delay reload" % vrid + elif int(value) and 0 <= int(value) <= 3600: + cmd = "vrrp %d delay reload %d" % (vrid, value) + else: + raise ValueError("vrrp property 'delay_reload' must be" + "in the range 0-3600 %r" % value) + + # 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: + True if the command executed successfully, error if failure. + If run is False: + 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: + 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) + 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 = '' + 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 = '' + 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: + True if the command executed successfully, error if failure. + If run is False: + The formatted command string which can be passed to the node. + + """ + + if default is True: + cmd = "default vrrp %d bfd ip" % vrid + elif disable is True or value is None: + cmd = "no vrrp %d bfd ip" % vrid + elif re.match(r'^\d+\.\d+\.\d+\.\d+$', str(value)): + cmd = "vrrp %d bfd ip %s" % (vrid, value) + else: + raise ValueError("vrrp property 'bfd_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 _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 is '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) diff --git a/pyeapi/client.py b/pyeapi/client.py index f8a775a..f2f4662 100644 --- a/pyeapi/client.py +++ b/pyeapi/client.py @@ -31,7 +31,7 @@ # """Python Client for eAPI -This module provides the client for eAPI. It provides to primary functions +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 @@ -347,6 +347,7 @@ def hosts_for_tag(tag): 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) @@ -375,7 +376,7 @@ def make_connection(transport, **kwargs): return klass(**kwargs) def connect(transport=None, host='localhost', username='admin', - password='', port=None): + password='', port=None, timeout=60): """ Creates a connection using the supplied settings This function will create a connection to an Arista EOS node using @@ -401,7 +402,7 @@ def connect(transport=None, host='localhost', username='admin', """ transport = transport or DEFAULT_TRANSPORT return make_connection(transport, host=host, username=username, - password=password, port=port) + password=password, port=port, timeout=timeout) class Node(object): @@ -504,7 +505,7 @@ def config(self, commands): commands = list(commands) # push the configure command onto the command stack - commands.insert(0, 'configure') + commands.insert(0, 'configure terminal') response = self.run_commands(commands) if self.autorefresh: @@ -581,10 +582,15 @@ def enable(self, commands, encoding='json', strict=False): 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) for index, response in enumerate(responses): results.append(dict(command=commands[index], + result=response, response=response, encoding=encoding)) else: @@ -745,7 +751,3 @@ def connect_to(name): port=kwargs.get('port')) node = Node(connection, **kwargs) return node - - - - diff --git a/pyeapi/eapilib.py b/pyeapi/eapilib.py index 7145b5f..012f0b6 100644 --- a/pyeapi/eapilib.py +++ b/pyeapi/eapilib.py @@ -61,11 +61,11 @@ DEFAULT_UNIX_SOCKET = '/var/run/command-api.sock' -def https_connection_factory(path, host, port, context=None): +def https_connection_factory(path, host, port, context=None, timeout=60): # ignore ssl context for python versions before 2.7.9 if sys.hexversion < 34015728: - return HttpsConnection(path, host, port) - return HttpsConnection(path, host, port, context=context) + return HttpsConnection(path, host, port, timeout=timeout) + return HttpsConnection(path, host, port, context=context, timeout=timeout) class EapiError(Exception): """Base exception class for all exceptions generated by eapilib @@ -156,9 +156,10 @@ def __init__(self, connection_type, message, commands=None): class SocketConnection(HTTPConnection): - def __init__(self, path): + def __init__(self, path, timeout=60): HTTPConnection.__init__(self, 'localhost') self.path = path + self.timeout = timeout def __str__(self): return 'unix:%s' % self.path @@ -168,6 +169,7 @@ def __repr__(self): 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): @@ -470,30 +472,30 @@ def execute(self, commands, encoding='json', **kwargs): raise class SocketEapiConnection(EapiConnection): - def __init__(self, path=None, **kwargs): + def __init__(self, path=None, timeout=60, **kwargs): super(SocketEapiConnection, self).__init__() path = path or DEFAULT_UNIX_SOCKET - self.transport = SocketConnection(path) + self.transport = SocketConnection(path, timeout) class HttpLocalEapiConnection(EapiConnection): - def __init__(self, port=None, path=None, **kwargs): + 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', port) + self.transport = HttpConnection(path, 'localhost', port, timeout=timeout) class HttpEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, - password=None, **kwargs): + 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, port) + self.transport = HttpConnection(path, host, port, timeout=timeout) self.authentication(username, password) class HttpsEapiConnection(EapiConnection): def __init__(self, host, port=None, path=None, username=None, - password=None, context=None, **kwargs): + password=None, context=None, timeout=60, **kwargs): super(HttpsEapiConnection, self).__init__() port = port or DEFAULT_HTTPS_PORT path = path or DEFAULT_HTTP_PATH @@ -503,7 +505,7 @@ def __init__(self, host, port=None, path=None, username=None, if context is None and not enforce_verification: context = self.disable_certificate_verification() - self.transport = https_connection_factory(path, host, port, context) + self.transport = https_connection_factory(path, host, port, context, timeout) self.authentication(username, password) def disable_certificate_verification(self): @@ -518,6 +520,3 @@ def disable_certificate_verification(self): # temporary until a proper fix is implemented. if hasattr(ssl, '_create_unverified_context'): return ssl._create_unverified_context() - - - diff --git a/setup.py b/setup.py index 257384b..8c65f1f 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ from setuptools import setup, find_packages from codecs import open -from os import path +from os import path, environ +import sys here = path.abspath(path.dirname(__file__)) @@ -71,3 +72,20 @@ '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)) diff --git a/test/fixtures/running_config.routemaps b/test/fixtures/running_config.routemaps new file mode 100644 index 0000000..e6ccd54 --- /dev/null +++ b/test/fixtures/running_config.routemaps @@ -0,0 +1,35 @@ +route-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 +! diff --git a/test/fixtures/running_config.text b/test/fixtures/running_config.text index af2c0c8..0a6666e 100644 --- a/test/fixtures/running_config.text +++ b/test/fixtures/running_config.text @@ -1586,6 +1586,9 @@ 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 @@ -1663,7 +1666,7 @@ no ip tacacs source-interface no vxlan vni notation dotted ! banner login -this +this is the loging ban that would b emult EOF diff --git a/test/fixtures/running_config.varp b/test/fixtures/running_config.varp new file mode 100644 index 0000000..b7737cd --- /dev/null +++ b/test/fixtures/running_config.varp @@ -0,0 +1,17 @@ +ip 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 +! diff --git a/test/fixtures/running_config.varp_null b/test/fixtures/running_config.varp_null new file mode 100644 index 0000000..be4d8a7 --- /dev/null +++ b/test/fixtures/running_config.varp_null @@ -0,0 +1,19 @@ +interface 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 +! diff --git a/test/fixtures/running_config.vrrp b/test/fixtures/running_config.vrrp new file mode 100644 index 0000000..d847eff --- /dev/null +++ b/test/fixtures/running_config.vrrp @@ -0,0 +1,533 @@ +! +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 +! +! diff --git a/test/system/test_api_interfaces.py b/test/system/test_api_interfaces.py index 3e293cf..7f3c638 100644 --- a/test/system/test_api_interfaces.py +++ b/test/system/test_api_interfaces.py @@ -163,6 +163,28 @@ def test_get_members_default(self): 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']: @@ -220,6 +242,38 @@ def test_set_members(self): 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_minimum_links_valid(self): diff --git a/test/system/test_api_mlag.py b/test/system/test_api_mlag.py index b77b65a..80775e4 100644 --- a/test/system/test_api_mlag.py +++ b/test/system/test_api_mlag.py @@ -142,6 +142,15 @@ def test_set_peer_link_with_value(self): 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']) diff --git a/test/system/test_api_routemaps.py b/test/system/test_api_routemaps.py new file mode 100644 index 0000000..b5eb3a7 --- /dev/null +++ b/test/system/test_api_routemaps.py @@ -0,0 +1,277 @@ +# +# 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 TEST2 permit 50', + 'route-map TEST2 permit 50', + 'match tag 50']) + result = dut.api('routemaps').getall() + self.assertIn(('TEST'), result) + self.assertIn(('TEST2'), 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, + value=None) + self.assertTrue(result) + self.assertEqual(None, api.get('TEST')['deny'][10]['continue']) + +if __name__ == '__main__': + unittest.main() diff --git a/test/system/test_api_staticroute.py b/test/system/test_api_staticroute.py new file mode 100644 index 0000000..5207b39 --- /dev/null +++ b/test/system/test_api_staticroute.py @@ -0,0 +1,291 @@ +# +# 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 is '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 is '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 delete-static-routes', + '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 delete-static-routes', + '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 delete-static-routes', + '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, routes) + + 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 delete-static-routes', + '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 delete-static-routes', + '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 delete-static-routes', + '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 delete-static-routes', + '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() diff --git a/test/system/test_api_stp.py b/test/system/test_api_stp.py index 273d1d1..5837072 100644 --- a/test/system/test_api_stp.py +++ b/test/system/test_api_stp.py @@ -49,7 +49,7 @@ def test_get(self): def test_getall(self): for dut in self.duts: - dut.config('default interface Et1-7') + dut.config('default interface Et1-4') result = dut.api('stp').interfaces.getall() self.assertIsInstance(result, dict) diff --git a/test/system/test_api_users.py b/test/system/test_api_users.py index c1db5e5..de46e75 100644 --- a/test/system/test_api_users.py +++ b/test/system/test_api_users.py @@ -37,15 +37,25 @@ 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: - dut.config(['no username test', 'username test nopassword']) + dut.config(['no username test', 'username test nopassword', + 'username test sshkey %s' % TEST_SSH_KEY]) + result = dut.api('users').get('test') values = dict(nopassword=True, privilege='1', secret='', - role='', format='') + role='', format='', + sshkey=TEST_SSH_KEY) result = self.sort_dict_by_keys(result) values = self.sort_dict_by_keys(values) @@ -85,16 +95,17 @@ def test_default(self): self.assertTrue(result) self.assertNotIn('username test nopassword', api.config) - def set_privilege_with_value(self): + def test_set_privilege_with_value(self): for dut in self.duts: dut.config(['no username test', 'username test nopassword']) api = dut.api('users') - self.assertIn('username test nopassword', api.config) + # EOS defaults to privilege 1 + self.assertIn('username test privilege 1 nopassword', api.config) result = api.set_privilege('test', 8) self.assertTrue(result) - self.assertNotIn('username test privilege 8', api.config) + self.assertIn('username test privilege 8 nopassword', api.config) - def set_privilege_with_no_value(self): + def test_set_privilege_with_no_value(self): for dut in self.duts: dut.config(['no username test', 'username test privilege 8 nopassword']) @@ -102,8 +113,67 @@ def set_privilege_with_no_value(self): self.assertIn('username test privilege 8', api.config) result = api.set_privilege('test') self.assertTrue(result) - self.assertNotIn('username test privilege 1', api.config) + 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) + self.assertNotIn('username test sshkey', api.config) + result = api.set_sshkey('test', TEST_SSH_KEY) + self.assertTrue(result) + 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) + self.assertNotIn('username test sshkey', api.config) + result = api.set_sshkey('test', '') + self.assertTrue(result) + 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) + self.assertNotIn('username test sshkey', api.config) + result = api.set_sshkey('test', None) + self.assertTrue(result) + 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') + self.assertTrue(result) + self.assertNotIn('username test sshkey %s' % TEST_SSH_KEY, + api.config) if __name__ == '__main__': unittest.main() diff --git a/test/system/test_api_varp.py b/test/system/test_api_varp.py new file mode 100644 index 0000000..25ce5c8 --- /dev/null +++ b/test/system/test_api_varp.py @@ -0,0 +1,228 @@ +# +# 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 + +VIRT_NULL = 'no ip virtual-router mac-address' +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 test_basic_get(self): + for dut in self.duts: + dut.config([VIRT_NULL]) + response = dut.api('varp').get() + self.assertIsNotNone(response) + + def test_get_with_value(self): + for dut in self.duts: + 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): + for dut in self.duts: + dut.config([VIRT_NULL]) + response = dut.api('varp').get() + self.assertIsNotNone(response) + self.assertEqual(response['mac_address'], None) + + def test_set_mac_address_with_value(self): + for dut in self.duts: + 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: + 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_set_mac_address_with_bad_value(self): + for dut in self.duts: + 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_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_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() diff --git a/test/system/test_api_vrrp.py b/test/system/test_api_vrrp.py new file mode 100644 index 0000000..fd1ee10 --- /dev/null +++ b/test/system/test_api_vrrp.py @@ -0,0 +1,499 @@ +# +# 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) + 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) + 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(['interface %s' % interface, + 'no vrrp %d' % 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 + self.assertEqual(response, vrrp_conf) + + def test_delete(self): + vrid = 101 + for dut in self.duts: + interface = self._vlan_setup(dut) + 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) + 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): + pass + 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(['interface %s' % interface, + 'no vrrp %d' % 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] + + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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) + 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() diff --git a/test/unit/test_api_bgp.py b/test/unit/test_api_bgp.py index 4faef7b..e321407 100644 --- a/test/unit/test_api_bgp.py +++ b/test/unit/test_api_bgp.py @@ -49,7 +49,8 @@ def __init__(self, *args, **kwargs): def test_get(self): result = self.instance.get() - keys = ['bgp_as', 'router_id', 'shutdown', 'neighbors', 'networks'] + keys = ['bgp_as', 'router_id', 'maximum_paths', 'maximum_ecmp_paths', + 'shutdown', 'neighbors', 'networks'] self.assertEqual(sorted(keys), sorted(result.keys())) def test_create(self): @@ -97,6 +98,35 @@ def test_set_router_id(self): func = function('set_router_id', rid, True) 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') + elif state == 'default': + cmds = ['router bgp 65000', 'default maximum-paths'] + func = function('set_maximum_paths', default=True) + 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') + elif state == 'default': + cmds = ['router bgp 65000', 'default maximum-paths'] + func = function('set_maximum_paths', default=True) + self.eapi_positive_config_test(func, cmds) + def test_set_shutdown(self): for state in ['config', 'negate', 'default']: if state == 'config': @@ -167,7 +197,7 @@ def test_set_remote_as(self): self.eapi_positive_config_test(func, cmds) def test_set_shutdown(self): - for state in ['config', 'negate', 'default']: + for state in ['config', 'negate', 'default', 'false']: name = 'test' cmd = 'neighbor {}'.format(name) if state == 'config': @@ -179,6 +209,9 @@ def test_set_shutdown(self): elif state == 'default': cmds = ['router bgp 65000', 'default {} shutdown'.format(cmd)] func = function('set_shutdown', name, False, True) + elif state == 'false': + cmds = ['router bgp 65000', 'no {} shutdown'.format(cmd)] + func = function('set_shutdown', name, False) self.eapi_positive_config_test(func, cmds) def test_set_send_community(self): @@ -262,5 +295,3 @@ def test_set_description(self): if __name__ == '__main__': unittest.main() - - diff --git a/test/unit/test_api_interfaces.py b/test/unit/test_api_interfaces.py index e7d8cbe..9a7f1bb 100644 --- a/test/unit/test_api_interfaces.py +++ b/test/unit/test_api_interfaces.py @@ -77,6 +77,10 @@ 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) @@ -303,6 +307,27 @@ def test_set_members(self): ['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']) @@ -410,5 +435,3 @@ def test_remove_vtep_from_vlan(self): if __name__ == '__main__': unittest.main() - - diff --git a/test/unit/test_api_routemaps.py b/test/unit/test_api_routemaps.py new file mode 100644 index 0000000..93afb65 --- /dev/null +++ b/test/unit/test_api_routemaps.py @@ -0,0 +1,146 @@ +# +# 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): + result = self.instance.getall() + self.assertIsInstance(result, dict) + + 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_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) + 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): + value = random_string() + cmds = ['route-map TEST permit 10', 'no description'] + func = function('set_description', 'TEST', 'permit', 10) + self.eapi_positive_config_test(func, cmds) + + def test_set_description_with_default(self): + value = random_string() + cmds = ['route-map TEST permit 10', 'default description'] + func = function('set_description', 'TEST', 'permit', 10, default=True) + self.eapi_positive_config_test(func, cmds) + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/test_api_staticroute.py b/test/unit/test_api_staticroute.py new file mode 100644 index 0000000..4cc507b --- /dev/null +++ b/test/unit/test_api_staticroute.py @@ -0,0 +1,304 @@ +# +# 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() diff --git a/test/unit/test_api_users.py b/test/unit/test_api_users.py index 00898d0..08278aa 100644 --- a/test/unit/test_api_users.py +++ b/test/unit/test_api_users.py @@ -52,7 +52,7 @@ def test_isprivilege_returns_false(self): self.assertFalse(result) def test_get(self): - keys = ['nopassword', 'privilege', 'role', 'secret', 'format'] + keys = ['nopassword', 'privilege', 'role', 'secret', 'format', 'sshkey'] result = self.instance.get('test') self.assertEqual(sorted(keys), sorted(result.keys())) @@ -104,7 +104,7 @@ def test_set_privilege(self): self.eapi_positive_config_test(func, cmds) def test_set_privilege_negate(self): - cmds = 'username test no privilege' + cmds = 'username test privilege 1' func = function('set_privilege', 'test') self.eapi_positive_config_test(func, cmds) @@ -118,17 +118,10 @@ def test_set_role(self): self.eapi_positive_config_test(func, cmds) def test_set_role_negate(self): - cmds = 'username test no role' + cmds = 'default username test role' func = function('set_role', 'test') self.eapi_positive_config_test(func, cmds) - - - - - if __name__ == '__main__': unittest.main() - - diff --git a/test/unit/test_api_varp.py b/test/unit/test_api_varp.py new file mode 100644 index 0000000..4375502 --- /dev/null +++ b/test/unit/test_api_varp.py @@ -0,0 +1,139 @@ +# +# 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.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_no_value(self): + func = function('set_mac_address', mac_address=None) + cmds = 'no ip virtual-router mac-address' + self.eapi_positive_config_test(func, cmds) + + 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' + 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) + +if __name__ == '__main__': + unittest.main() diff --git a/test/unit/test_api_vrrp.py b/test/unit/test_api_vrrp.py new file mode 100644 index 0000000..d4948c9 --- /dev/null +++ b/test/unit/test_api_vrrp.py @@ -0,0 +1,806 @@ +# +# 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}], + ] + + 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() diff --git a/test/unit/test_client.py b/test/unit/test_client.py index e27ccef..123f1b3 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -194,7 +194,7 @@ def test_hosts_for_tag_returns_names(self): def test_connect_types(self, connection): transports = list(pyeapi.client.TRANSPORTS.keys()) kwargs = dict(host='localhost', username='admin', password='', - port=None) + port=None, timeout=60) for transport in transports: pyeapi.client.connect(transport) @@ -250,7 +250,7 @@ def test_connect_default_type(self): with patch.dict(pyeapi.client.TRANSPORTS, {'https': transport}): pyeapi.client.connect() kwargs = dict(host='localhost', username='admin', password='', - port=None) + port=None, timeout=60) transport.assert_called_once_with(**kwargs) def test_connect_to_with_config(self): @@ -260,7 +260,7 @@ def test_connect_to_with_config(self): pyeapi.client.load_config(filename=conf) pyeapi.client.connect_to('test1') kwargs = dict(host='192.168.1.16', username='eapi', - password='password', port=None) + password='password', port=None, timeout=60) transport.assert_called_once_with(**kwargs)