diff --git a/.gitignore b/.gitignore index 0a1085b..c9d4a6e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ Gemfile.lock rpmbuild rpms *.spec + +# Other +/spec/fixtures/dut.conf diff --git a/CHANGELOG.md b/CHANGELOG.md index c88f90c..24b152b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,26 @@ Ruby Client for eAPI ==================== +## v0.5.0, January, 2016 + +- Add optional ‘mode’ parameter to set_members() method in port-channel + interfaces API +- Add support for trunk groups +- Ensure multiple connections based on the wildcard settings do not clobber + each other. +- Add ‘terminal’ to the ‘configure’ command to workaround AAA issue +- Fix issue where ‘enablepw’ in the eapi.conf was not properly used +- Catch errors and syslog them when parsing eapi conf file. + In the event of an unparsable eapi.conf, a syslog warning will be generated + but the app will continue to attempt to utilize the default localhost conn. +- Ensure that nil is returned when getting nonexistent username +- Ensure all parse methods are private +- Add tests for timeout values +- Update framework tests +- Add unit tests for switchports +- Address code coverage gaps + + ## v0.4.0, November, 2015 - New users API diff --git a/Gemfile b/Gemfile index 4d206b8..548e7a7 100644 --- a/Gemfile +++ b/Gemfile @@ -24,7 +24,7 @@ group :development, :test do gem 'pry', require: false gem 'pry-doc', require: false gem 'pry-stack_explorer', require: false - gem 'rbeapi', '0.4.0', path: '.' + gem 'rbeapi', '0.5.0', path: '.' gem 'ci_reporter_rspec', require: false gem 'simplecov-json', require: false gem 'simplecov-rcov', require: false diff --git a/README.md b/README.md index e0790cb..311a565 100644 --- a/README.md +++ b/README.md @@ -230,21 +230,21 @@ Copy the RPMs to an EOS device then run the 'swix create' command. Examples: Puppet Open Source: cd /mnt/flash; \ - swix create rbeapi-0.4.0-1.swix \ - rubygem-rbeapi-0.4.0-1.eos4.noarch.rpm \ + swix create rbeapi-0.5.0-1.swix \ + rubygem-rbeapi-0.5.0-1.eos4.noarch.rpm \ rubygem-inifile-3.0.0-3.eos4.noarch.rpm \ rubygem-netaddr-1.5.0-2.eos4.noarch.rpm \ rubygem-net_http_unix-0.2.1-3.eos4.noarch.rpm Puppet-enterprise agent (3.x): cd/mnt/flash; \ - swix create rbeapi-puppet3-0.4.0-1.swix \ - rubygem-rbeapi-puppet3-0.4.0-1.eos4.noarch.rpm \ + swix create rbeapi-puppet3-0.5.0-1.swix \ + rubygem-rbeapi-puppet3-0.5.0-1.eos4.noarch.rpm \ rubygem-inifile-puppet3-3.0.0-3.eos4.noarch.rpm \ rubygem-netaddr-puppet3-1.5.0-2.eos4.noarch.rpm Puppet-All-in-one agent (2015.x/4.x): cd/mnt/flash; \ - swix create rbeapi-puppet-aio-0.4.0-1.swix \ - rubygem-rbeapi-puppet-aio-0.4.0-1.eos4.noarch.rpm \ + swix create rbeapi-puppet-aio-0.5.0-1.swix \ + rubygem-rbeapi-puppet-aio-0.5.0-1.eos4.noarch.rpm \ rubygem-inifile-puppet-aio-3.0.0-3.eos4.noarch.rpm \ rubygem-netaddr-puppet-aio-1.5.0-2.eos4.noarch.rpm \ rubygem-net_http_unix-puppet-aio-0.2.1-3.eos4.noarch.rpm @@ -255,13 +255,13 @@ Copy the RPMs to an EOS device then run the 'swix create' command. Arista# copy flash: Arista# bash -bash-4.1# cd /mnt/flash/ - -bash-4.1# swix create rbeapi-puppet3-0.4.0-1.swix \ - rubygem-rbeapi-puppet3-0.4.0-1.eos4.noarch.rpm \ + -bash-4.1# swix create rbeapi-puppet3-0.5.0-1.swix \ + rubygem-rbeapi-puppet3-0.5.0-1.eos4.noarch.rpm \ rubygem-inifile-puppet3-3.0.0-1.eos4.noarch.rpm \ rubygem-netaddr-puppet3-1.5.0-1.eos4.noarch.rpm -bash-4.1# exit - Arista# copy flash:rbeapi-puppet3-0.4.0-1.swix extension: - Arista# extension rbeapi-puppet3-0.4.0-1.swix + Arista# copy flash:rbeapi-puppet3-0.5.0-1.swix extension: + Arista# extension rbeapi-puppet3-0.5.0-1.swix Arista# copy installed-extensions boot-extensions ``` @@ -270,7 +270,7 @@ Copy the RPMs to an EOS device then run the 'swix create' command. On EOS: ``` Arista# no extension pe-rbeapi-0.3.0-1.swix - Arista# extension rbeapi-puppet3-0.4.0-1.swix + Arista# extension rbeapi-puppet3-0.5.0-1.swix Arista# copy installed-extensions boot-extensions ``` diff --git a/Rakefile b/Rakefile index 27626c2..f41752a 100644 --- a/Rakefile +++ b/Rakefile @@ -155,3 +155,22 @@ task ci_spec: [:ci_prep, 'ci:setup:rspec', :spec] require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) + +desc 'Generate typedoc.rst for the guide' +task :typedoc do + system 'rbeapi doc -r type \ + | awk \'/This page/{flag=1}/augeas/{flag=0}/eos_/{flag=1}/ exec/\ + {flag=0}/\*This page/{flag=1}flag\' \ + | pandoc --from=markdown --to=rst --output=- \ + > guide/typedoc.rst' +end + +desc 'Generate Getting Started Guide HTML' +task guide: [:typedoc] do + system 'make -C guide html' +end + +desc 'Clean Getting Started docs' +task :guide_clean do + system 'make -C guide clean' +end diff --git a/guide/Makefile b/guide/Makefile new file mode 100644 index 0000000..1985178 --- /dev/null +++ b/guide/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Rbeapi.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Rbeapi.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Rbeapi" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Rbeapi" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/guide/_static/arista_logo_11-trans-w.png b/guide/_static/arista_logo_11-trans-w.png new file mode 100644 index 0000000..dec3830 Binary files /dev/null and b/guide/_static/arista_logo_11-trans-w.png differ diff --git a/guide/_static/arista_logo_jpg-11.jpg b/guide/_static/arista_logo_jpg-11.jpg new file mode 100644 index 0000000..5f025ef Binary files /dev/null and b/guide/_static/arista_logo_jpg-11.jpg differ diff --git a/guide/_static/favicon.ico b/guide/_static/favicon.ico new file mode 100644 index 0000000..cf55b0c Binary files /dev/null and b/guide/_static/favicon.ico differ diff --git a/guide/conf.py b/guide/conf.py new file mode 100644 index 0000000..8335d42 --- /dev/null +++ b/guide/conf.py @@ -0,0 +1,279 @@ +# -*- coding: utf-8 -*- +# +# Rbeapi documentation build configuration file, created by +# sphinx-quickstart on Fri Jan 8 11:04:02 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# on_rtd is whether we are on readthedocs.org, this line of code grabbed from docs.readthedocs.org +on_rtd = os.environ.get('READTHEDOCS', None) == 'True' + +if not on_rtd: # only import and set the theme if we're building docs locally + import sphinx_rtd_theme + html_theme = 'sphinx_rtd_theme' + html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Rbeapi' +copyright = u'2016, Arista Networks - EOS+ Consulting Services' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +import re +p = re.compile("version\": \"(.*)\"") +with open("../metadata.json") as f: + data = f.read() + ver = p.search(data) + if ver: + version = ver.group(1) + +#print "Version: {}".format(version) + +# The short X.Y version. +# version = '0.5.0' +# The full version, including alpha/beta/rc tags. +# release = '0.5.0' +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +html_logo = '_static/arista_logo_11-trans-w.png' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = '_static/favicon.ico' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'rbeapidoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'rbeapi.tex', u'Rbeapi Documentation', + u'Arista Networks - EOS+ Consulting Services', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +latex_logo = '_static/arista_logo_jpg-11.jpg' + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'rbeapi', u'Rbeapi Documentation', + [u'Arista Networks - EOS+ Consulting Services'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Rbeapi', u'Rbeapi Documentation', + u'Arista Networks - EOS+ Consulting Services', 'Rbeapi', 'Ruby API for EOS.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/guide/cookbook.rst b/guide/cookbook.rst new file mode 100644 index 0000000..c5535bd --- /dev/null +++ b/guide/cookbook.rst @@ -0,0 +1,4 @@ +Cookbook +============ + +.. contents:: :local: \ No newline at end of file diff --git a/guide/developing.rst b/guide/developing.rst new file mode 100644 index 0000000..5e7e5e4 --- /dev/null +++ b/guide/developing.rst @@ -0,0 +1,4 @@ +Developing +========== + +.. contents:: :local: \ No newline at end of file diff --git a/guide/faq.rst b/guide/faq.rst new file mode 100644 index 0000000..eb5392d --- /dev/null +++ b/guide/faq.rst @@ -0,0 +1,4 @@ +FAQ +=== + +.. contents:: :local: \ No newline at end of file diff --git a/guide/index.rst b/guide/index.rst new file mode 100644 index 0000000..fb21811 --- /dev/null +++ b/guide/index.rst @@ -0,0 +1,23 @@ +.. Rbeapi documentation master file, created by + sphinx-quickstart on Fri Jan 8 11:04:02 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Rbeapi's documentation! +================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + overview + quickstart + installation + cookbook + troubleshooting + developing + testing + faq + release-notes + license \ No newline at end of file diff --git a/guide/installation.rst b/guide/installation.rst new file mode 100644 index 0000000..3c4aa04 --- /dev/null +++ b/guide/installation.rst @@ -0,0 +1,4 @@ +Installation +============ + +.. contents:: :local: \ No newline at end of file diff --git a/guide/license.rst b/guide/license.rst new file mode 100644 index 0000000..df32f7b --- /dev/null +++ b/guide/license.rst @@ -0,0 +1,5 @@ +License +======= + +.. include:: ../LICENSE + :literal: \ No newline at end of file diff --git a/guide/overview.rst b/guide/overview.rst new file mode 100644 index 0000000..d97ec29 --- /dev/null +++ b/guide/overview.rst @@ -0,0 +1,20 @@ +Overview +======== + +.. contents:: :local: + +Introduction +------------ + +The Ruby Client for eAPI provides a native Ruby implementation for programming Arista EOS network devices using Ruby. The Ruby client provides the ability to build native applications in Ruby that can communicate with EOS either locally via Unix domain sockets (on-box) or remotely over a HTTP/S transport (off-box). It uses a standard INI-style configuration file to specifiy one or more connection profiles. + +The rbeapi implemenation also provides an API layer for building native Ruby objects that allow for configuration and state extraction of EOS nodes. The API layer provides a consistent implementation for working with EOS configuration resources. The implementation of the API layer is highly extensible and can be used as a foundation for building custom data models. + +The libray is freely provided to the open source community for building robust applications using Arista EOS eAPI. Support is provided as best effort through Github iusses. + +Prerequisites +------------- + +* Arista EOS 4.12 or later +* Arista eAPI enabled for at least one transport (see official EOS Config Guide at arista.com for details) +* Ruby 1.9.3 or later \ No newline at end of file diff --git a/guide/quickstart.rst b/guide/quickstart.rst new file mode 100644 index 0000000..41feb59 --- /dev/null +++ b/guide/quickstart.rst @@ -0,0 +1,4 @@ +Quick Start +=========== + +.. contents:: :local: \ No newline at end of file diff --git a/guide/release-notes-0.5.0.rst b/guide/release-notes-0.5.0.rst new file mode 100644 index 0000000..13c3b4e --- /dev/null +++ b/guide/release-notes-0.5.0.rst @@ -0,0 +1,60 @@ +Release 0.5.0 - January 2016 +---------------------------- + +.. contents:: :local: + +Enhancements +^^^^^^^^^^^^ + +* Add lacp_mode option when setting port-channel members. (`89 `_) [`devrobo `_] + .. comment +* Add support for trunk groups. (`88 `_) [`devrobo `_] + .. comment +* Unit tests for switchports (`94 `_) [`websitescenes `_] + .. comment +* Ensure all parse methods are private. (`93 `_) [`websitescenes `_] + .. comment +* Add tests for timeout values (`92 `_) [`websitescenes `_] + .. comment +* Relax check on getall entries (`91 `_) [`devrobo `_] + .. comment +* Update framework tests (`90 `_) [`websitescenes `_] + .. comment +* Add basic framework tests. (`85 `_) [`websitescenes `_] + .. comment +* Address code coverage gaps (`84 `_) [`websitescenes `_] + .. comment + +Fixed +^^^^^ + +* Copy configuration entry before modifying with connection specific info. (`101 `_) + .. comment +* Add 'terminal' to configure command to work around AAA issue. (`99 `_) + .. comment +* Set enable password for a connection. (`96 `_) + .. comment +* Catch errors and syslog them when parsing eapi conf file. (`95 `_) + In the event of an unparsable ``eapi.conf`` file, which could occur due to other tools which used a YAML syntax instead of INI, rbeapi will log a warning via syslog, but continue to attempt a default connection to localhost. + .. comment +* Ensure that nil is returned when getting nonexistent username. (`83 `_) + .. comment +* Failure when eapi.conf is not formatted correctly (`82 `_) + In the event of an unparsable ``eapi.conf`` file, which could occur due to other tools which used a YAML syntax instead of INI, rbeapi will log a warning via syslog, but continue to attempt a default connection to localhost. +* Enable password setting in the .eapi.conf file not honored (`72 `_) + ``enablepwd`` is now properly used, if defined, in the ``eapi.conf`` +* API interfaces should accept an lacp_mode to configure for port-channel members (`58 `_) + ``set_members()`` now configures LACP mode when adding members to a port-channel + +Known Caveats +^^^^^^^^^^^^^ + +* Add support for commands with input (`100 `_) + .. comment +* Wildcard connection config gets clobbered (`86 `_) + .. comment +* Need to validate value keyword in set methods when array (`40 `_) + .. comment +* get_connect should raise an error instead of returning nil if no connection is found (`31 `_) + .. comment + diff --git a/guide/release-notes.rst b/guide/release-notes.rst new file mode 100644 index 0000000..31b3783 --- /dev/null +++ b/guide/release-notes.rst @@ -0,0 +1,6 @@ +Release Notes +============= + +.. toctree:: + :maxdepth: 2 + :titlesonly: \ No newline at end of file diff --git a/guide/testing.rst b/guide/testing.rst new file mode 100644 index 0000000..79ab0db --- /dev/null +++ b/guide/testing.rst @@ -0,0 +1,4 @@ +Testing Modules +=============== + +.. contents:: :local: \ No newline at end of file diff --git a/guide/troubleshooting.rst b/guide/troubleshooting.rst new file mode 100644 index 0000000..3fa493d --- /dev/null +++ b/guide/troubleshooting.rst @@ -0,0 +1 @@ +.. _troubleshooting: \ No newline at end of file diff --git a/lib/rbeapi/api/aaa.rb b/lib/rbeapi/api/aaa.rb index f6289c7..16dd75b 100644 --- a/lib/rbeapi/api/aaa.rb +++ b/lib/rbeapi/api/aaa.rb @@ -35,18 +35,39 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Aaa class manages Authorization, Authentication and Accounting (AAA) # on an EOS node. class Aaa < Entity + ## + # get returns a hash of all Aaa resources + # + # @example + # { + # : { + # : { + # type: , + # servers: + # }, + # : { + # type: , + # servers: + # } + # } + # } + # + # @return [Hash] Returns the Aaa resources as a + # Hash. If no Aaa resources are found, an empty hash is returned. def get response = {} response[:groups] = groups.getall response end + ## + # Returns an object node for working with AaaGroups class. def groups return @groups if @groups @groups = AaaGroups.new node @@ -55,20 +76,20 @@ def groups end ## - # The AaaGroups class manages the server groups on a EOS node. + # The AaaGroups class manages the server groups on an EOS node. class AaaGroups < Entity DEFAULT_RADIUS_AUTH_PORT = 1812 DEFAULT_RADIUS_ACCT_PORT = 1813 - # Regular express that parses the radius servers from the aaa group - # server radius configuration block + # Regular expression that parses the radius servers from the aaa group + # server radius configuration block. RADIUS_GROUP_SERVER = /\s{3}server [ ]([^\s]+) [ ]auth-port[ ](\d+) [ ]acct-port[ ](\d+)/x - # Regular expression that parse the tacacs servers from the aaa group - # server tacacs+ configuration block + # Regular expression that parses the tacacs servers from the aaa group + # server tacacs+ configuration block. TACACS_GROUP_SERVER = /\s{3}server [ ]([^\s]+) (?:[ ]vrf[ ](\w+))? @@ -76,15 +97,15 @@ class AaaGroups < Entity ## # get returns the aaa server group resource hash that describes the - # current configuration for the specified server group name + # current configuration for the specified server group name. # - # The resource hash returned contains the following: - # * type: (String) The server group type. Valid values are either - # 'tacacs' or 'radius' - # * servers: (Array) The set of servers associated with the group. - # Servers are returned as either IP address or host name + # @example + # { + # type: , + # servers: + # } # - # @param [String] :name The server group name to return f:rom the nodes + # @param [String] :name The server group name to return from the nodes # current running configuration. If the name is not configured a nil # object is returned. # @@ -99,6 +120,23 @@ def get(name) response end + ## + # getall returns a aaa server groups hash + # + # @example + # { + # : { + # type: , + # servers: + # }, + # : { + # type: , + # servers: + # } + # } + # + # @return [Hash] returns the resource hashes for + # configured aaa groups. If none exist, a nil object is returned def getall cfg = config.scan(/aaa group server (?:radius|tacacs\+) (.+)$/) cfg.each_with_object({}) do |name, hsh| @@ -109,7 +147,7 @@ def getall ## # parse_type scans the specified configuration block and returns the - # server group type as either 'tacacs' or 'radius' The type value is + # server group type as either 'tacacs' or 'radius'. The type value is # expected to always be present in the config. # # @api private @@ -160,7 +198,6 @@ def parse_servers(config, type) # @param [String] :config The aaa server group block configuration for the # group name to parse # - # # @return [Hash] resource hash attribute def parse_radius_server(config) values = config.scan(RADIUS_GROUP_SERVER).map do |(name, auth, acct)| @@ -175,8 +212,8 @@ def parse_radius_server(config) private :parse_radius_server ## - # parse_tacacs_server scans the provide configuration block and returns - # the list of servers configured. The configuration block is expected to + # parse_tacacs_server scans the provided configuration block and returns + # the list of configured servers. The configuration block is expected to # be a tacacs configuration block. If there are no servers configured # for the group the servers value will return an empty array. # @@ -185,7 +222,6 @@ def parse_radius_server(config) # @param [String] :config The aaa server group block configuration for the # group name to parse # - # # @return [Hash] resource hash attribute def parse_tacacs_server(config) values = config.scan(TACACS_GROUP_SERVER).map do |(name, vrf, port)| diff --git a/lib/rbeapi/api/acl.rb b/lib/rbeapi/api/acl.rb index f4a591b..dafd5f7 100644 --- a/lib/rbeapi/api/acl.rb +++ b/lib/rbeapi/api/acl.rb @@ -33,7 +33,7 @@ require 'rbeapi/api' ## -# Eos is the toplevel namespace for working with Arista EOS nodes +# Rbeapi toplevel namespace module Rbeapi ## # Api is module namespace for working with the EOS command API @@ -56,10 +56,29 @@ def initialize(node) ## # get returns the specified ACL from the nodes current configuration. # + # @example + # { + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # ... + # } + # # @param [String] :name The ACL name. # # @return [nil, Hash] Returns the ACL resource as a - # Hash. + # Hash. Returns nil if name does not exist. def get(name) config = get_block("ip access-list standard #{name}") return nil unless config @@ -72,6 +91,45 @@ def get(name) # configuration as a hash. The ACL resource collection hash is # keyed by the ACL name. # + # @example + # { + # : { + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # ... + # }, + # : { + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # : { + # seqno: , + # action: , + # srcaddr: , + # srcprefixle: , + # log: + # }, + # ... + # }, + # ... + # } + # # @return [nil, Hash] Returns a hash that represents # the entire ACL collection from the nodes running configuration. # If there are no ACLs configured, this method will return an diff --git a/lib/rbeapi/api/bgp.rb b/lib/rbeapi/api/bgp.rb index 079e897..67d3a63 100644 --- a/lib/rbeapi/api/bgp.rb +++ b/lib/rbeapi/api/bgp.rb @@ -51,6 +51,50 @@ def initialize(node) # get returns the BGP routing configuration from the nodes current # configuration. # + # @example + # { + # bgp_as: , + # router_id: , + # shutdown: , + # maximum_paths: , + # maximum_ecmp_paths: + # networks: [ + # { + # prefix: , + # masklen: , + # route_map: + # }, + # { + # prefix: , + # masklen: , + # route_map: + # } + # ], + # neighbors: { + # name: { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: , + # next_hop_selp: , + # route_map_in: , + # route_map_out: + # }, + # name: { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: , + # next_hop_selp: , + # route_map_in: , + # route_map_out: + # }, + # ... + # } + # } + # # @return [nil, Hash] Returns the BGP resource as a # Hash. def get @@ -397,6 +441,18 @@ class BgpNeighbors < Entity # get returns a single BGP neighbor entry from the nodes current # configuration. # + # @example + # { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: + # next_hop_self: + # route_map_in: + # route_map_out: + # } + # # @param [String] :name The name of the BGP neighbor to manage. # This value can be either an IPv4 address or string (in the # case of managing a peer group). @@ -422,6 +478,31 @@ def get(name) # getall returns the collection of all neighbor entries for the # BGP router instance. # + # @example + # { + # : { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: + # next_hop_self: + # route_map_in: + # route_map_out: + # }, + # : { + # peer_group: , + # remote_as: , + # send_community: , + # shutdown: , + # description: + # next_hop_self: + # route_map_in: + # route_map_out: + # }, + # ... + # } + # # @return [nil, Hash] Returns a hash that # represents the entire BGP neighbor collection from the nodes # running configuration. If there a BGP router is not configured diff --git a/lib/rbeapi/api/dns.rb b/lib/rbeapi/api/dns.rb index 787443d..f3777cb 100644 --- a/lib/rbeapi/api/dns.rb +++ b/lib/rbeapi/api/dns.rb @@ -35,13 +35,13 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Dns class manages DNS settings on an EOS node. class Dns < Entity ## - # Returns the DNS resource + # get returns the DNS resource # # @example # { @@ -60,21 +60,43 @@ def get response end + ## + # parse_domain_name parses the domain-name from config + # + # @api private + # + # @return [Hash] resource hash attribute def parse_domain_name mdata = /ip domain-name ([\w.]+)/.match(config) { domain_name: mdata.nil? ? '' : mdata[1] } end + private :parse_domain_name + ## + # parse_name_servers parses the name-server values from + # config + # + # @api private + # + # @return [Hash] resource hash attribute def parse_name_servers servers = config.scan(/(?:ip name-server vrf )(?:\w+)\s(.+)/) values = servers.each_with_object([]) { |srv, arry| arry << srv.first } { name_servers: values } end + private :parse_name_servers + ## + # parse_domain_list parses the domain-list from config + # + # @api private + # + # @return [Hash] resource hash attribute def parse_domain_list search = config.scan(/(?<=^ip\sdomain-list\s).+$/) { domain_list: search } end + private :parse_domain_list ## # Configure the domain-name value in the running-config @@ -135,10 +157,22 @@ def set_name_servers(opts = {}) configure cmds end + ## + # add_name_server adds an ip name-server. + # + # @param [String] :server The name of the ip name-server to create + # + # @return [Boolean] returns true if the command completed successfully def add_name_server(server) configure "ip name-server #{server}" end + ## + # remove_name_server removes the specified ip name-server. + # + # @param [String] :server The name of the ip name-server to remove + # + # @return [Boolean] returns true if the command completed successfully def remove_name_server(server) configure "no ip name-server #{server}" end @@ -190,10 +224,22 @@ def set_domain_list(opts = {}) configure cmds end + ## + # add_domain_list adds an ip domain-list. + # + # @param [String] :name The name of the ip domain-list to add + # + # @return [Boolean] returns true if the command completed successfully def add_domain_list(name) configure "ip domain-list #{name}" end + ## + # remove_domain_list removes a specified ip domain-list. + # + # @param [String] :name The name of the ip domain-list to remove + # + # @return [Boolean] returns true if the command completed successfully def remove_domain_list(name) configure "no ip domain-list #{name}" end diff --git a/lib/rbeapi/api/interfaces.rb b/lib/rbeapi/api/interfaces.rb index d154219..f2e2de3 100644 --- a/lib/rbeapi/api/interfaces.rb +++ b/lib/rbeapi/api/interfaces.rb @@ -36,7 +36,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api module namespace + # Api is module namespace for working with the EOS command API module Api ## # The Interfaces class manages all physical and logical interfaces on an @@ -47,10 +47,52 @@ def initialize(node) @instances = {} end + ## + # get returns a hash of interface configurations for the given name + # + # @example + # { + # name: , + # type: , + # description: , + # shutdown: + # } + # + # @param [String] :name The interface name to return a resource for from + # the nodes configuration + # + # @return [nil, Hash] Returns the interface resource as a + # Hash. If the specified name is not found in the nodes current + # configuration a nil object is returned def get(name) get_instance(name).get(name) end + ## + # getall returns a hash of interface configurations + # + # @example + # { + # : { + # name: , + # type: , + # description: , + # shutdown: , + # ... + # }, + # : { + # name: , + # type: , + # description: , + # shutdown: , + # ... + # }, + # ... + # } + # + # @return [Hash] Returns the interface resources as a + # Hash. If none exist in the nodes current + # configuration an empty hash is returned def getall interfaces = config.scan(/(?<=^interface\s).+$/) @@ -60,6 +102,13 @@ def getall end end + ## + # get_instance returns an interface instance for the given name + # + # @param [String] :name The interface name to return an instance for + # + # @return [Object] Returns the interface instance as an + # Object. def get_instance(name) name = name[0, 2].upcase case name @@ -287,20 +336,18 @@ class EthernetInterface < BaseInterface # get returns the specified Ethernet interface resource hash that # represents the interface's current configuration in the node. # - # The resource hash returned contains the following information: - # - # * name (string): the interface name (eg Ethernet1) - # * type (string): will always be 'ethernet' - # * description (string): the interface description value - # * speed (string): the current speed setting for the interface speed - # * forced (boolean): true if auto negotiation is disabled otherwise - # false - # * sflow (boolean): true if sflow is enabled on the interface - # otherwise false - # * flowcontrol_send (string): the interface flowcontrol send value. - # Valid values are 'on' or 'off' - # * flowconrol_receive (string): the interface flowcontrol receive - # value. Valid values are 'on' or 'off' + # @example + # { + # name: , + # type: , + # description: , + # shutdown: , + # speed: , + # forced: , + # sflow: , + # flowcontrol_send: , + # flowcontrol_receive: + # } # # @param [String] :name The interface name to return a resource hash # for from the node's running configuration @@ -319,7 +366,6 @@ def get(name) response.merge!(parse_sflow(config)) response.merge!(parse_flowcontrol_send(config)) response.merge!(parse_flowcontrol_receive(config)) - response end @@ -773,24 +819,41 @@ def set_minimum_links(name, opts = {}) # @param [Array] :members The array of physical interface members to add # to the port-channel logical interface. # + # @param [str] :mode 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. + # # @return [Boolean] returns true if the command completed successfully - def set_members(name, members) + def set_members(name, members, mode = nil) current_members = Set.new parse_members(name)[:members] members = Set.new members + lacp_mode = parse_lacp_mode(name)[:lacp_mode] + if mode && mode != lacp_mode + lacp_mode = mode + set_lacp_mode(name, lacp_mode) + end + + cmds = [] + grpid = /(\d+)/.match(name)[0] + # remove members from the current port-channel interface current_members.difference(members).each do |intf| - result = remove_member(name, intf) - return false unless result + cmds << "interface #{intf}" + cmds << "no channel-group #{grpid}" end # add new member interfaces to the port-channel members.difference(current_members).each do |intf| - result = add_member(name, intf) - return false unless result + cmds << "interface #{intf}" + cmds << "channel-group #{grpid} mode #{lacp_mode}" end - true + configure(cmds) end ## @@ -938,16 +1001,18 @@ class VxlanInterface < BaseInterface # BaseInterface get method and adds the Vxlan specific attributes to # the hash # - # The returned resource hash contains the following - # - # * name: (String) The full interface name identifier - # * type: (String) 'vxlan' - # * description: (String) The configured interface description - # * shutdown: (Boolean) The admin state of the interface - # * source_interface: (String) The vxlan source-interface value - # * multicast_group: (String) The vxlan multicast-group value - # * udp_port: (Fixnum) The vxlan udp-port value - # * flood_list: (Array) The list of VTEPs to flood traffic towards + # @example + # { + # name: , + # type: , + # description: , + # shutdown: , + # source_interface: , + # multicast_group: , + # udp_port: , + # flood_list: , + # vlans: + # } # # @param [String] :name The interface name to return from the nodes # configuration. This optional parameter defaults to Vxlan1 diff --git a/lib/rbeapi/api/ipinterfaces.rb b/lib/rbeapi/api/ipinterfaces.rb index 568f3ac..ddeb80c 100644 --- a/lib/rbeapi/api/ipinterfaces.rb +++ b/lib/rbeapi/api/ipinterfaces.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api # # The Ipinterface class provides an instance for managing logical @@ -79,7 +79,17 @@ def get(name) # # @example # { - # : {...} + # : { + # address: + # mtu: + # helper_addresses: array + # }, + # : { + # address: + # mtu: + # helper_addresses: array + # }, + # ... # } # # @see get Ipaddress resource example @@ -103,6 +113,7 @@ def getall # ipaddress resource hash. # # @api private + # # @param [String] :config The IP interface configuration block returned # from the node's running configuration # diff --git a/lib/rbeapi/api/logging.rb b/lib/rbeapi/api/logging.rb index 05e1f99..f5b18c1 100644 --- a/lib/rbeapi/api/logging.rb +++ b/lib/rbeapi/api/logging.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Logging class manages logging settings on an EOS node. @@ -46,7 +46,7 @@ class Logging < Entity # # @example # { - # enable: [true, false] + # enable: [true, false], # hosts: array # } # @@ -65,6 +65,10 @@ def get # command is expected to always be in the node's configuration. This # methods return value is intended to be merged into the logging resource # hash. + # + # @api private + # + # @return [Hash] resource hash attribute def parse_enable value = /no logging on/ !~ config { enable: value } @@ -76,10 +80,15 @@ def parse_enable # logging hosts are configured, then the value for hosts will be an empty # array. The return value is intended to be merged into the logging # resource hash + # + # @api private + # + # @return [Hash] resource hash attribute def parse_hosts hosts = config.scan(/(?<=^logging\shost\s)[^\s]+/) { hosts: hosts } end + private :parse_hosts ## # set_enable configures the global logging instance on the node as either diff --git a/lib/rbeapi/api/mlag.rb b/lib/rbeapi/api/mlag.rb index 99779b7..e88a67f 100644 --- a/lib/rbeapi/api/mlag.rb +++ b/lib/rbeapi/api/mlag.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Mlag class provides a configuration instance for working with @@ -50,15 +50,25 @@ class Mlag < Entity # get scans the current nodes configuration and returns the values as # a Hash describing the current state. # - # The resource hash returned contains the following: - # * domain_id: (String) The MLAG domain-id value - # * local_interface: (String) The MLAG local-interface value - # * peer_address: (String) The IP address of the MLAG peer - # * peer_link: (String) The MLAG transit peer-link value - # * shutdown: (Boolean) The administrative staet of the mlag - # configuration - # * interfaces: (Hash) The list of configured MLAG interfaces. (See - # parse_interfaces for the Hash details) + # @example + # { + # global: { + # domain_id: , + # local_interface: , + # peer_address: , + # peer_link: , + # shutdown: + # }, + # interfaces: { + # : { + # mlag_id: + # }, + # : { + # mlag_id: + # }, + # ... + # } + # } # # @see parse_interfaces # diff --git a/lib/rbeapi/api/ntp.rb b/lib/rbeapi/api/ntp.rb index e1d756d..2b1e78d 100644 --- a/lib/rbeapi/api/ntp.rb +++ b/lib/rbeapi/api/ntp.rb @@ -32,10 +32,10 @@ require 'rbeapi/api' ## -# Eos is the toplevel namespace for working with Arista EOS nodes +# Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # The Ntp class provides an instance for working with the nodes @@ -48,7 +48,7 @@ class Ntp < Entity # # @example # { - # source_interface: + # source_interface: , # servers: { # prefer: [true, false] # } @@ -95,6 +95,7 @@ def parse_servers end { servers: values } end + private :parse_servers ## # set_source_interface configures the ntp source value in the nodes diff --git a/lib/rbeapi/api/ospf.rb b/lib/rbeapi/api/ospf.rb index 46486af..8f4fea4 100644 --- a/lib/rbeapi/api/ospf.rb +++ b/lib/rbeapi/api/ospf.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Ospf class is a global class that provides an instance for working @@ -48,13 +48,15 @@ class Ospf < Entity # # @example # { - # "router_id": - # "areas": { + # router_id: + # areas: { # : array # }, - # "redistribute" + # redistribute: {} # } # + # @param [String] :inst The ospf instance name + # # @return [Hash] A Ruby hash object that provides the OSPF settings as # key / value pairs. def get(inst) @@ -90,9 +92,16 @@ def get(inst) # # @example # { - # : {...} - # "interfaces": {...} + # : { + # router_id: , + # areas: {}, + # redistribute: {} + # }, + # interfaces: {} # } + # + # @return [Hash] A Ruby hash object that provides the OSPF settings as + # key / value pairs. def getall instances = config.scan(/(?<=^router\sospf\s)\d+$/) response = instances.each_with_object({}) do |inst, hsh| @@ -108,28 +117,91 @@ def interfaces @interfaces end + ## + # create will create a router ospf with the specified pid + # + # @param [String] :pid The router ospf to create + # + # @return [Boolean] returns true if the command completed successfully def create(pid) configure "router ospf #{pid}" end + ## + # delete will remove the specified router ospf + # + # @param [String] :pid The router ospf to remove + # + # @return [Boolean] returns true if the command completed successfully def delete(pid) configure "no router ospf #{pid}" end + ## + # set_router_id sets router ospf router-id with pid and options + # + # @param [String] :pid The router ospf name + # + # @param [hash] :opts Optional keyword arguments + # + # @option :opts [Boolean] :enable If false then the command is + # negated. Default is true. + # + # @option :opts [Boolean] :default Configure the router-id to default. + # + # @return [Boolean] returns true if the command completed successfully def set_router_id(pid, opts = {}) cmd = command_builder('router-id', opts) cmds = ["router ospf #{pid}", cmd] configure cmds end + ## + # add_network adds network settings for router ospf and network area. + # + # @param [String] :pid The pid for router ospf + # + # @param [String] :net The network name + # + # @param [String] :area The network area name + # + # @return [Boolean] returns true if the command completed successfully def add_network(pid, net, area) configure ["router ospf #{pid}", "network #{net} area #{area}"] end + ## + # remove_network removes network settings for router ospf and network + # area. + # + # @param [String] :pid The pid for router ospf + # + # @param [String] :net The network name + # + # @param [String] :area The network area name + # + # @return [Boolean] returns true if the command completed successfully def remove_network(pid, net, area) configure ["router ospf #{pid}", "no network #{net} area #{area}"] end + ## + # set_redistribute sets router ospf router-id with pid and options + # + # @param [String] :pid The router ospf name + # + # @param [String] :proto The redistribute value + # + # @param [hash] :opts Optional keyword arguments + # + # @option :opts [String] :routemap The route-map value + # + # @option :opts [Boolean] :enable If false then the command is + # negated. Default is true. + # + # @option :opts [Boolean] :default Configure the router-id to default. + # + # @return [Boolean] returns true if the command completed successfully def set_redistribute(pid, proto, opts = {}) routemap = opts[:routemap] cmds = ["router ospf #{pid}", "redistribute #{proto}"] @@ -147,8 +219,7 @@ class OspfInterfaces < Entity # # Example # { - # "name": , - # "network_type": + # network_type: # } # # @param [String] :name The interface name to return the configuration @@ -174,8 +245,13 @@ def get(name) # # Example # { - # : {...}, - # : {...} + # : { + # network_type: + # }, + # : { + # network_type: + # }, + # ... # } # # @return [nil, Hash] A Ruby hash that represents the @@ -189,6 +265,22 @@ def getall end end + ## + # set_network_type sets network type with options + # + # @param [String] :name The name of the interface + # + # @param [hash] :opts Optional keyword arguments + # + # @option :opts [String] :value The point-to-point value + # + # @option :opts [Boolean] :enable If false then the command is + # negated. Default is true. + # + # @option :opts [Boolean] :default Configure the ip ospf network + # to default. + # + # @return [Boolean] returns true if the command completed successfully def set_network_type(name, opts = {}) value = opts[:value] return false unless [nil, 'point-to-point'].include?(value) diff --git a/lib/rbeapi/api/prefixlists.rb b/lib/rbeapi/api/prefixlists.rb index 379819c..0a2435d 100644 --- a/lib/rbeapi/api/prefixlists.rb +++ b/lib/rbeapi/api/prefixlists.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Prefixlists class provides a configuration instance for working @@ -48,16 +48,17 @@ class Prefixlists < Entity # @example # { # : { - # "next_hop": , - # "name": + # next_hop: , + # name: # } # } # + # @param [String] :name The name of the prefix-list to return + # # @returns [Hash The method will return all of the # configured static routes on the node as a Ruby hash object. If # there are no static routes configured, this method will return # an empty hash - def get(name) config = get_block("ip prefix-list #{name}") return nil unless config @@ -69,6 +70,21 @@ def get(name) end end + ## + # Returns the static routes configured on the node + # + # @example + # { + # : { + # next_hop: , + # name: + # } + # } + # + # @returns [Hash The method will return all of the + # configured static routes on the node as a Ruby hash object. If + # there are no static routes configured, this method will return + # an empty hash def getall lists = config.scan(/(?<=^ip\sprefix-list\s).+/) lists.each_with_object({}) do |name, hsh| @@ -77,10 +93,29 @@ def getall end end + ## + # create will create a new ip prefix-list with designated name. + # + # @param [String] :name The name of the ip prefix-list + # + # @return [Boolean] returns true if the command completed successfully def create(name) configure "ip prefix-list #{name}" end + ## + # add_rule will create an ip prefix-list with the designated name, + # seqno, action and prefix. + # + # @param [String] :name The name of the ip prefix-list + # + # @param [String] :seq The seq value + # + # @param [String] :action The action value + # + # @param [String] :prefix The prefix value + # + # @return [Boolean] returns true if the command completed successfully def add_rule(name, action, prefix, seq = nil) cmd = "ip prefix-list #{name}" cmd << " seq #{seq}" if seq @@ -88,6 +123,14 @@ def add_rule(name, action, prefix, seq = nil) configure cmd end + ## + # delete will remove the designated prefix-list + # + # @param [String] :name The name of the ip prefix-list + # + # @param [String] :seq The seq value + # + # @return [Boolean] returns true if the command completed successfully def delete(name, seq = nil) cmd = "no ip prefix-list #{name}" cmd << " seq #{seq}" if seq diff --git a/lib/rbeapi/api/radius.rb b/lib/rbeapi/api/radius.rb index 86c1481..33b449a 100644 --- a/lib/rbeapi/api/radius.rb +++ b/lib/rbeapi/api/radius.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # Radius provides instance methods to retrieve and set radius configuration @@ -61,14 +61,14 @@ class Radius < Entity # This method is intended to be used by a provider's instances class # method. # - # The resource hash returned contains the following information: - # * key: (String) the key either in plain text or hashed format - # * key_format: (Fixnum) e.g. 0 or 7 - # * timeout: (Fixnum) seconds before the timeout period ends - # * retransmit: (Fixnum), e.g. 3, attempts after first timeout expiry. - # * servers: (Array), - # - # @api public + # @example + # { + # key: , + # key_format: , + # timeout: , + # retransmit: , + # servers: + # } # # @return [Array] Single element Array of resource hashes def get diff --git a/lib/rbeapi/api/routemaps.rb b/lib/rbeapi/api/routemaps.rb index ca6494c..7be95b3 100644 --- a/lib/rbeapi/api/routemaps.rb +++ b/lib/rbeapi/api/routemaps.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Routemaps class manages routemaps. A route map is a list of rules @@ -81,8 +81,8 @@ class Routemaps < Entity # } # } # - # @param [String] name The routemap name to return a resource for from the - # nodes configuration + # @param [String] :name The routemap name to return a resource for from + # the nodes configuration # # @return [nil, Hash] Returns the routemap resource as a # Hash. If the specified name is not found in the nodes current @@ -174,10 +174,11 @@ def getall ## # parse entries is a private method to get the routemap rules. # + # @api private + # # @return [nil, Hash] returns a hash that represents the # rules for routemaps from the nodes running configuration. If # there are no routemaps configured, this method will return nil. - # def parse_entries(name) entries = config.scan(/^route-map\s#{name}\s.+$/) return nil if entries.empty? @@ -196,11 +197,12 @@ def parse_entries(name) ## # parse rule is a private method to parse a rule. # + # @api private + # # @return [Hash] returns a hash that represents the # rules for routemaps from the nodes running configuration. If # there are no routemaps configured, this method will return an empty # hash. - # def parse_rules(rules) rules.split("\n").each_with_object({}) do |rule, rule_hsh| mdata = /\s{3}(\w+)\s/.match(rule) diff --git a/lib/rbeapi/api/snmp.rb b/lib/rbeapi/api/snmp.rb index fd2ea60..3006260 100644 --- a/lib/rbeapi/api/snmp.rb +++ b/lib/rbeapi/api/snmp.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namespace for working with eAPI abstractions + # Api is module namespace for working with the EOS command API module Api ## # The Snmp class provides a class implementation for working with the @@ -57,9 +57,9 @@ class Snmp < Entity # # @example # { - # location: - # contact: - # chassis_id: + # location: , + # contact: , + # chassis_id: , # source_interface: # } # @@ -403,6 +403,15 @@ def set_community_acl(name, opts = {}) configure cmds end + ## + # set_community_access configures snmp-server community with designated + # name and access values. + # + # @param [String] :name The snmp-server community name value + # + # @param [String] :access The snmp-server community access value + # + # @return [Boolean] returns true if the command completed successfully def set_community_access(name, access) configure "snmp-server community #{name} #{access}" end diff --git a/lib/rbeapi/api/staticroutes.rb b/lib/rbeapi/api/staticroutes.rb index 739aa64..1efe459 100644 --- a/lib/rbeapi/api/staticroutes.rb +++ b/lib/rbeapi/api/staticroutes.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Staticroutes class provides a configuration instance for working diff --git a/lib/rbeapi/api/stp.rb b/lib/rbeapi/api/stp.rb index 79def53..bd2a25f 100644 --- a/lib/rbeapi/api/stp.rb +++ b/lib/rbeapi/api/stp.rb @@ -34,7 +34,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Stp class provides a base class instance for working with @@ -48,13 +48,20 @@ class Stp < Entity # @example # { # mode: - # instances: {...} - # interfaces: {...} + # instances: { + # : { + # priority: + # } + # } + # interfaces: { + # : { + # portfast: , + # portfast_type: , + # bpduguard: + # } + # } # } # - # @see StpInstances instances get example - # @eee StpInterfaces interfaces get example - # # @return [Hash] returns a Hash of attributes derived from eAPI def get response = {} @@ -150,6 +157,8 @@ class StpInstances < Entity # priority: # } # + # @param [String] :inst The named stp instance to return + # # @return [nil, Hash: {...} + # : { + # priority: + # }, + # : { + # priority: + # }, + # ... # } # - # @see get Instance get example - # # @return [Hash] returns all configured stp instances # found in the nodes running configuration def getall @@ -265,10 +278,14 @@ class StpInterfaces < Entity # # @example # { - # portfast: [true, false] - # bpduguard: [true, false] + # portfast: , + # portfast_type: , + # bpduguard: # } # + # @param [String] :name The interface name to return a resource for from + # the nodes configuration + # # @return [nil, Hash] returns the stp interface as a # resource hash def get(name) @@ -289,11 +306,19 @@ def get(name) # # @example # { - # : {...} + # : { + # portfast: , + # portfast_type: , + # bpduguard: + # }, + # : { + # portfast: , + # portfast_type: , + # bpduguard: + # }, + # ... # } # - # @see get Interface example - # # @return [Hash] returns the stp interfaces config as a # resource hash from the nodes running configuration def getall diff --git a/lib/rbeapi/api/switchports.rb b/lib/rbeapi/api/switchports.rb index 373b7d5..b10ed7b 100644 --- a/lib/rbeapi/api/switchports.rb +++ b/lib/rbeapi/api/switchports.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # The Switchport class provides a base class instance for working with @@ -55,7 +55,7 @@ class Switchports < Entity # "access_vlan": # } # - # @param [String] name The full name of the interface to get. The + # @param [String] :name The full name of the interface to get. The # interface name must be the full interface (ie Ethernet, not Et) # # @return [Hash] a hash that includes the switchport properties @@ -69,24 +69,67 @@ def get(name) response.merge!(parse_access_vlan(config)) response.merge!(parse_trunk_native_vlan(config)) response.merge!(parse_trunk_allowed_vlans(config)) + response.merge!(parse_trunk_groups(config)) response end + ## + # parse_mode parses switchport mode from the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_mode(config) mdata = /(?<=\s{3}switchport\smode\s)(.+)$/.match(config) { mode: mdata[1] } end + private :parse_mode + ## + # parse_access_vlan parses access vlan from the provided + # config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_access_vlan(config) mdata = /(?<=access\svlan\s)(.+)$/.match(config) { access_vlan: mdata[1] } end + private :parse_access_vlan + ## + # parse_trunk_native_vlan parses trunk native vlan from + # the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_trunk_native_vlan(config) mdata = /(?<=trunk\snative\svlan\s)(.+)$/.match(config) { trunk_native_vlan: mdata[1] } end + private :parse_trunk_native_vlan + ## + # parse_trunk_allowed_vlans parses trunk allowed vlan from + # the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_trunk_allowed_vlans(config) mdata = /(?<=trunk\sallowed\svlan\s)(.+)$/.match(config) return { trunk_allowed_vlans: [] } unless mdata[1] != 'none' @@ -101,10 +144,47 @@ def parse_trunk_allowed_vlans(config) end { trunk_allowed_vlans: values } end + private :parse_trunk_allowed_vlans + + ## + # parse_trunk_groups parses trunk group values from the + # provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute + def parse_trunk_groups(config) + mdata = config.scan(/(?<=trunk\sgroup\s)(.+)$/) + mdata = mdata.flatten if mdata.length > 0 + { trunk_groups: mdata } + end + private :parse_trunk_groups ## # Retrieves all switchport interfaces from the running-config # + # @example + # { + # : { + # mode: , + # access_vlan: , + # trunk_native_vlan: , + # trunk_allowed_vlans: , + # trunk_groups: + # }, + # : { + # mode: , + # access_vlan: , + # trunk_native_vlan: , + # trunk_allowed_vlans: , + # trunk_groups: + # }, + # ... + # } + # # @return [Array] an array of switchport hashes def getall interfaces = config.scan(/(?<=^interface\s)([Et|Po].+)$/) @@ -248,6 +328,50 @@ def set_access_vlan(name, opts = {}) cmd = command_builder('switchport access vlan', opts) configure_interface(name, cmd) end + + ## + # Configures the trunk group vlans for the specified interface. + # Trunk groups not currently set are added and trunk groups + # currently configured but not in the passed in value array are removed. + # + # @param [String] name The name of the interface to configure + # @param [Hash] opts The configuration parameters for the interface + # @option opts [string] :value Set of values to configure the trunk group + # @option opts [Boolean] :enable If false then the command is + # negated. Default is true. + # @option opts [Boolean] :default The value should be set to default + # Default takes precedence over enable. + # + # @return [Boolean] True if the commands succeed otherwise False + def set_trunk_groups(name, opts = {}) + default = opts.fetch(:default, false) + if default + return configure_interface(name, 'default switchport trunk group') + end + + enable = opts.fetch(:enable, true) + unless enable + return configure_interface(name, 'no switchport trunk group') + end + + value = opts.fetch(:value, []) + fail ArgumentError, 'value must be an Array' unless value.is_a?(Array) + + value = Set.new value + current_value = Set.new get(name)[:trunk_groups] + + cmds = [] + # Add trunk groups that are not currently in the list + value.difference(current_value).each do |group| + cmds << "switchport trunk group #{group}" + end + + # Remove trunk groups that are not in the new list + current_value.difference(value).each do |group| + cmds << "no switchport trunk group #{group}" + end + configure_interface(name, cmds) if cmds.length > 0 + end end end end diff --git a/lib/rbeapi/api/system.rb b/lib/rbeapi/api/system.rb index 34b0e06..063529a 100644 --- a/lib/rbeapi/api/system.rb +++ b/lib/rbeapi/api/system.rb @@ -35,18 +35,19 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The System class configures the node system services such as # hostname and domain name class System < Entity ## - # Returns the system settings + # Returns the system settings for hostname and iprouting # # @example # { - # hostname: + # hostname: , + # iprouting: # } # # @return [Hash] A Ruby hash object that provides the system settings as @@ -58,15 +59,35 @@ def get response end + ## + # parse_hostname parses hostname values from the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_hostname(config) mdata = /(?<=^hostname\s)(.+)$/.match(config) { hostname: mdata.nil? ? '' : mdata[1] } end + private :parse_hostname + ## + # parse_iprouting parses ip routing from the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_iprouting(config) mdata = /no\sip\srouting/.match(config) { iprouting: mdata.nil? ? true : false } end + private :parse_iprouting ## # Configures the system hostname value in the running-config diff --git a/lib/rbeapi/api/tacacs.rb b/lib/rbeapi/api/tacacs.rb index a955349..4b34ae5 100644 --- a/lib/rbeapi/api/tacacs.rb +++ b/lib/rbeapi/api/tacacs.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # Tacacs provides instance methods to retrieve and set tacacs configuration @@ -63,15 +63,14 @@ class Tacacs < Entity # This method is intended to be used by a provider's instances class # method. # - # The resource hash returned contains the following information: - # * name: ('settings') - # * enable: (true | false) if tacacs functionality is enabled. This is - # always true for EOS. - # * key: (String) the key either in plain text or hashed format - # * key_format: (Integer) e.g. 0 or 7 - # * timeout: (Integer) seconds before the timeout period ends - # - # @api public + # @example + # { + # name: , + # enable: , + # key: , + # key_format: , + # timeout: + # } # # @return [Array] Single element Array of resource hashes def get diff --git a/lib/rbeapi/api/users.rb b/lib/rbeapi/api/users.rb index c546f9a..593157e 100644 --- a/lib/rbeapi/api/users.rb +++ b/lib/rbeapi/api/users.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # The Users class provides configuration of local user resources for @@ -76,7 +76,7 @@ def initialize(node) # sshkey: # } # - # @param [String] name The user name to return a resource for from the + # @param [String] :name The user name to return a resource for from the # nodes configuration # # @return [nil, Hash] Returns the user resource as a @@ -94,7 +94,7 @@ def get(name) (username\s+#{name}\s+ sshkey\s+(?.*)$)?/x) user = config.scan(user_re) - return nil unless user + return nil unless user && user[0] parse_user_entry(user[0]) end @@ -114,6 +114,15 @@ def get(name) # secret: , # sshkey: # }, + # : { + # name: , + # privilege: , + # role: , + # nopassword: , + # encryption: <'cleartext', 'md5', 'sha512'> + # secret: , + # sshkey: + # }, # ... # ] # diff --git a/lib/rbeapi/api/varp.rb b/lib/rbeapi/api/varp.rb index 47d6d53..45a4dcd 100644 --- a/lib/rbeapi/api/varp.rb +++ b/lib/rbeapi/api/varp.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Rbeapi::Api + # Api is module namespace for working with the EOS command API module Api ## # The Varp class provides an instance for working with the global @@ -44,10 +44,18 @@ class Varp < Entity ## # Returns the global VARP configuration from the node # - # Example + # @example # { - # "mac_address": , - # "interfaces": {...} + # mac_address: , + # interfaces: { + # : { + # addresses: + # }, + # : { + # addresses: + # }, + # ... + # } # } # # @return [Hash] A Ruby hash object that provides the Varp settings as @@ -59,6 +67,15 @@ def get response end + ## + # parse_mac_address parses mac-address values from the provided config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_mac_address(config) # ip virtual-router mac-address value will always # be stored in aa:bb:cc:dd:ee:ff format @@ -97,7 +114,7 @@ class VarpInterfaces < Entity ## # Returns a single VARP interface configuration # - # Example + # @example # { # "addresses": array # } @@ -119,10 +136,15 @@ def get(name) # Returns the collection of MLAG interfaces as a hash index by the # interface name # - # Example + # @example # { - # "name": {...}, - # "name": {...} + # : { + # addresses: + # }, + # : { + # addresses: + # }, + # ... # } # # @return [nil, Hash] A Ruby hash that represents the @@ -138,6 +160,16 @@ def getall end end + ## + # parse_addresses parses ip virtual-router address from the provided + # config + # + # @api private + # + # @param [String] :config The configuration block returned + # from the node's running configuration + # + # @return [Hash] resource hash attribute def parse_addresses(config) addrs = config.scan(/(?<=\s{3}ip\svirtual-router\saddress\s).+$/) { addresses: addrs } diff --git a/lib/rbeapi/api/vlans.rb b/lib/rbeapi/api/vlans.rb index 5ce3ad2..5567030 100644 --- a/lib/rbeapi/api/vlans.rb +++ b/lib/rbeapi/api/vlans.rb @@ -35,7 +35,7 @@ # Rbeapi toplevel namespace module Rbeapi ## - # Api is module namesapce for working with the EOS command API + # Api is module namespace for working with the EOS command API module Api ## # The Vlan class provides a class implementation for working with the @@ -50,12 +50,12 @@ class Vlans < Entity # # @example # { - # name: - # state: + # name: , + # state: , # trunk_groups: array[] Returns the vlan resource as a @@ -78,7 +78,17 @@ def get(id) # # @example # { - # : {...} + # : { + # name: , + # state: , + # trunk_groups: array[: { + # name: , + # state: , + # trunk_groups: array[ exc + Rbeapi::Utils.syslog_warning("#{exc}: in eapi conf file: #{filename}") + return + end # For each section, if the host parameter is omitted then the # connection name is used @@ -252,13 +259,14 @@ def get_connection(name) end ## - # Adds a new connection section to the current configuration + # Adds a new connection section to the current configuration # # @param [String] :name The name of the connection to add to the # configuration. # @param [Hash] :values The properties for the connection def add_connection(name, values) self["connection:#{name}"] = values + nil end end @@ -332,7 +340,7 @@ def enable_authentication(password) def config(commands, opts = {}) commands = [*commands] unless commands.respond_to?('each') - commands.insert(0, 'configure') + commands.insert(0, 'configure terminal') if @dry_run puts '[rbeapi dry-run commands]' diff --git a/lib/rbeapi/eapilib.rb b/lib/rbeapi/eapilib.rb index c16bce2..9472e67 100644 --- a/lib/rbeapi/eapilib.rb +++ b/lib/rbeapi/eapilib.rb @@ -162,6 +162,14 @@ def timeouts(opts = {}) @read_timeout = opts.fetch(:read_timeout, DEFAULT_HTTP_READ_TIMEOUT) end + ## + # Gets values for open_timeout and read_timeout + # + # @return [Hash] open_timeout and read_timeout + def get_timeouts + { open_timeout: @open_timeout, read_timeout: @read_timeout } + end + ## # Generates the eAPI JSON request message. # diff --git a/lib/rbeapi/utils.rb b/lib/rbeapi/utils.rb index 348e05b..d5337ab 100644 --- a/lib/rbeapi/utils.rb +++ b/lib/rbeapi/utils.rb @@ -30,6 +30,8 @@ # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # +require 'syslog' + ## # Rbeapi toplevel namespace module Rbeapi @@ -64,5 +66,13 @@ def self.class_from_string(name) mod.const_get(cls) end end + + ## + # Syslogs a warning message. + # + # @param [String] :message The message to log. + def self.syslog_warning(message) + Syslog.open('rbeapi', Syslog::LOG_PID) { |s| s.warning message } + end end end diff --git a/lib/rbeapi/version.rb b/lib/rbeapi/version.rb index d179c37..53c83dc 100644 --- a/lib/rbeapi/version.rb +++ b/lib/rbeapi/version.rb @@ -33,5 +33,5 @@ # # # Rbeapi toplevel namespace module Rbeapi - VERSION = '0.4.0' + VERSION = '0.5.0' end diff --git a/spec/fixtures/eapi.conf.yaml b/spec/fixtures/eapi.conf.yaml new file mode 100644 index 0000000..9102805 --- /dev/null +++ b/spec/fixtures/eapi.conf.yaml @@ -0,0 +1,6 @@ +--- +:username: admin +:password: admin +:use_ssl: true +:port: 199 +:hostname: bogus diff --git a/spec/fixtures/empty.conf b/spec/fixtures/empty.conf new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/env_path.conf b/spec/fixtures/env_path.conf new file mode 100644 index 0000000..d5a5d12 --- /dev/null +++ b/spec/fixtures/env_path.conf @@ -0,0 +1,5 @@ +[connection:env_path] +host: 172.16.131.40 +username: admin +password: admin +transport: https diff --git a/spec/fixtures/test.conf b/spec/fixtures/test.conf new file mode 100644 index 0000000..595eda7 --- /dev/null +++ b/spec/fixtures/test.conf @@ -0,0 +1,39 @@ +[connection:veos01] +username: eapi +password: password +transport: http +host: veos01 + +[connection:veos02] +transport: http +host: veos02 + +[connection:veos03] +transport: socket +host: veos03 + +[connection:veos04] +host: 172.16.10.1 +username: eapi +password: password +enablepwd: itsasecret +port: 1234 +transport: https + +[connection:veos05] +host: 172.16.131.40 +username: admin +password: admin +enablepwd: password +transport: https +port: 1234 +open_timeout: 12 +read_timeout: 12 + +[connection: localhost] +transport: http_local +host: localhost + +[connection:localhost] +transport: socket +host: localhost diff --git a/spec/fixtures/wildcard.conf b/spec/fixtures/wildcard.conf new file mode 100644 index 0000000..69a3e65 --- /dev/null +++ b/spec/fixtures/wildcard.conf @@ -0,0 +1,43 @@ +[connection:veos01] +username: eapi +password: password +transport: http +host: veos01 + +[connection:veos02] +transport: http +host: veos02 + +[connection:veos03] +transport: socket +host: veos03 + +[connection:veos04] +host: 172.16.10.1 +username: eapi +password: password +enablepwd: itsasecret +port: 1234 +transport: https + +[connection:veos05] +host: 172.16.131.40 +username: admin +password: admin +enablepwd: password +transport: https +port: 1234 +open_timeout: 12 +read_timeout: 12 + +[connection: localhost] +transport: http_local +host: localhost + +[connection:localhost] +transport: socket +host: localhost + +[connection:*] +username: foo +password: bar diff --git a/spec/system/rbeapi/api/aaa_groups_spec.rb b/spec/system/rbeapi/api/aaa_groups_spec.rb new file mode 100644 index 0000000..0eb9a61 --- /dev/null +++ b/spec/system/rbeapi/api/aaa_groups_spec.rb @@ -0,0 +1,122 @@ +# +# 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. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/aaa' + +describe Rbeapi::Api::AaaGroups do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + let(:all) do + { + 'blah' => { + type: 'radius', + servers: [] + }, + 'blahtwo' => { + type: 'radius', + servers: [] + } + } + end + + let(:blah) do + { + type: 'radius', + servers: [] + } + end + + let(:blahthree) do + { + type: 'tacacs+', + servers: [] + } + end + + let(:servers) do + [{ + name: 'localhost', + auth_port: '1812', + acct_port: '1813' + }] + end + + describe '#get' do + before do + node.config(['no aaa group server radius blah', + 'no aaa group server radius blahtwo', + 'no aaa group server tacacs+ blahthree', + 'aaa group server radius blah', 'exit', + 'aaa group server radius blahtwo', 'exit']) + end + + it 'returns the resource for given name' do + expect(subject.get('blah')).to eq(blah) + end + end + + describe '#getall' do + it 'returns all of the aaa group resources' do + expect(subject.getall).to eq(all) + end + end + + describe '#create' do + it 'adds a new aaa group' do + expect(subject.create('blahthree', 'tacacs+')).to eq(true) + expect(subject.get('blahthree')).to eq(blahthree) + end + end + + describe '#delete' do + it 'removes specified aaa group' do + expect(subject.get('blahthree')).to eq(blahthree) + expect(subject.delete('blahthree')).to eq(true) + expect(subject.get('blahthree')).to eq(nil) + end + end + + describe '#set_servers' do + it 'removes all servers and then adds one' do + expect(subject.set_servers('blahtwo', [{ name: 'localhost' }])) + .to eq(true) + expect(subject.get('blahtwo')[:servers]).to eq(servers) + end + end +end diff --git a/spec/system/rbeapi/api/aaa_spec.rb b/spec/system/rbeapi/api/aaa_spec.rb new file mode 100644 index 0000000..3b37571 --- /dev/null +++ b/spec/system/rbeapi/api/aaa_spec.rb @@ -0,0 +1,90 @@ +# +# 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. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/aaa' + +describe Rbeapi::Api::Aaa do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + let(:test) do + { + groups: { + 'blah' => { + type: 'radius', + servers: [] + }, + 'blahtwo' => { + type: 'radius', + servers: [] + } + } + } + end + + describe '#get' do + before do + node.config(['no aaa group server radius blah', + 'no aaa group server radius blahtwo', + 'aaa group server radius blah', 'exit', + 'aaa group server radius blahtwo', 'exit']) + end + + it 'returns the resource for given name' do + expect(subject.get).to eq(test) + end + + it 'returns a hash' do + expect(subject.get).to be_a_kind_of(Hash) + end + + it 'has two entries' do + expect(subject.get[:groups].size).to eq(2) + end + end + + describe '#groups' do + it 'returns new node instance' do + expect(subject.groups).to be_a_kind_of(Rbeapi::Api::AaaGroups) + end + + it 'returns a hash' do + expect(subject.groups).to be_a_kind_of(Object) + end + end +end diff --git a/spec/system/api_acl_spec.rb b/spec/system/rbeapi/api/acl_spec.rb similarity index 100% rename from spec/system/api_acl_spec.rb rename to spec/system/rbeapi/api/acl_spec.rb diff --git a/spec/system/rbeapi/api/bgp_neighbors_spec.rb b/spec/system/rbeapi/api/bgp_neighbors_spec.rb new file mode 100644 index 0000000..9a54027 --- /dev/null +++ b/spec/system/rbeapi/api/bgp_neighbors_spec.rb @@ -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. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/bgp' + +describe Rbeapi::Api::BgpNeighbors do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + describe '#get' do + let(:entity) do + { peer_group: nil, + remote_as: nil, + send_community: false, + shutdown: false, + description: nil, + next_hop_self: false, + route_map_in: nil, + route_map_out: nil } + end + + before do + node.config(['no router bgp 64600', 'router bgp 64600', + 'neighbor eBGP_GROUP peer-group']) + end + + it 'returns the BGP neighbor resource' do + expect(subject.get('eBGP_GROUP')).to eq(entity) + end + end + + describe '#getall' do + let(:entity) do + { + 'eBGP_GROUP' => { + peer_group: nil, remote_as: nil, send_community: false, + shutdown: false, description: nil, next_hop_self: false, + route_map_in: nil, route_map_out: nil + }, + '192.168.255.1' => { + peer_group: 'eBGP_GROUP', remote_as: '65000', send_community: true, + shutdown: true, description: nil, next_hop_self: true, + route_map_in: nil, route_map_out: nil + }, + '192.168.255.3' => { + peer_group: 'eBGP_GROUP', remote_as: '65001', send_community: true, + shutdown: true, description: nil, next_hop_self: true, + route_map_in: nil, route_map_out: nil + } + } + end + + before do + node.config(['no router bgp 64600', 'router bgp 64600', + 'neighbor 192.168.255.1 peer-group eBGP_GROUP', + 'neighbor 192.168.255.1 remote-as 65000', + 'neighbor 192.168.255.3 peer-group eBGP_GROUP', + 'neighbor 192.168.255.3 remote-as 65001']) + end + + it 'returns all the neighbors' do + expect(subject.getall).to eq(entity) + end + + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Hash) + end + + it 'has three entries' do + expect(subject.getall.size).to eq(3) + end + end + + describe '#create' do + let(:before) do + { peer_group: nil, + remote_as: nil, + send_community: true, + shutdown: true, + description: nil, + next_hop_self: true, + route_map_in: nil, + route_map_out: nil } + end + + let(:after) do + { peer_group: nil, + remote_as: nil, + send_community: false, + shutdown: true, + description: nil, + next_hop_self: false, + route_map_in: nil, + route_map_out: nil } + end + + before { node.config(['no router bgp 64600', 'router bgp 64600']) } + + it 'create a new BGP neighbor' do + expect(subject.get('edge')).to eq(before) + expect(subject.create('edge')).to be_truthy + + expect(subject.get('edge')).to eq(after) + end + end + + describe '#delete' do + let(:before) do + { peer_group: nil, + remote_as: nil, + send_community: false, + shutdown: true, + description: nil, + next_hop_self: false, + route_map_in: nil, + route_map_out: nil } + end + + let(:after) do + { peer_group: nil, + remote_as: nil, + send_community: true, + shutdown: true, + description: nil, + next_hop_self: true, + route_map_in: nil, + route_map_out: nil } + end + + it 'delete a BGP resource' do + expect(subject.get('edge')).to eq(before) + expect(subject.delete('edge')).to be_truthy + + expect(subject.get('edge')).to eq(after) + end + end + + describe '#set_peer_group' do + before do + node.config(['no router bgp 64600', 'router bgp 64600', + 'neighbor eBGP_GROUP peer-group']) + end + + it 'set the peer group' do + expect(subject.get('192.168.255.1')[:peer_group]).to eq(nil) + expect(subject.set_peer_group('192.168.255.1', value: 'eBGP_GROUP')) + .to be_truthy + expect(subject.get('192.168.255.1')[:peer_group]).to eq('eBGP_GROUP') + end + + it 'remove the peer group value' do + expect(subject.set_peer_group('192.168.255.1', value: 'eBGP_GROUP')) + .to be_truthy + expect(subject.get('192.168.255.1')[:peer_group]).to eq('eBGP_GROUP') + expect(subject.set_peer_group('192.168.255.1', enable: false)) + .to be_truthy + expect(subject.get('192.168.255.1')[:peer_group]).to eq(nil) + end + + it 'defaults the peer group value' do + expect(subject.set_peer_group('192.168.255.1', value: 'eBGP_GROUP')) + .to be_truthy + expect(subject.set_peer_group('192.168.255.1', default: true)) + .to be_truthy + expect(subject.get('192.168.255.1')[:peer_group]).to eq(nil) + end + end + + describe '#set_remote_as' do + it 'set the remote AS value' do + expect(subject.get('eng')[:remote_as]).to eq(nil) + expect(subject.set_remote_as('eng', value: '10')).to be_truthy + expect(subject.get('eng')[:remote_as]).to eq('10') + end + + it 'remove the remote AS value' do + expect(subject.get('eng')[:remote_as]).to eq('10') + expect(subject.set_remote_as('eng', enable: false)) + .to be_truthy + expect(subject.get('eng')[:remote_as]).to eq(nil) + end + + it 'defaults the remote AS value' do + expect(subject.set_remote_as('eng', value: '10')).to be_truthy + expect(subject.set_remote_as('eng', default: true)) + .to be_truthy + expect(subject.get('eng')[:remote_as]).to eq(nil) + end + end + + describe '#set_shutdown' do + it 'shutdown neighbor' do + expect(subject.get('eng')[:shutdown]).to eq(false) + expect(subject.set_shutdown('eng')).to be_truthy + expect(subject.get('eng')[:shutdown]).to eq(true) + end + + it 'negate shutdown neighbor' do + expect(subject.get('eng')[:shutdown]).to eq(true) + expect(subject.set_shutdown('eng', enable: false)).to be_truthy + expect(subject.get('eng')[:shutdown]).to eq(true) + end + + it 'default shutdown neighbor' do + expect(subject.get('eng')[:shutdown]).to eq(true) + expect(subject.set_shutdown('eng', default: true)).to be_truthy + expect(subject.get('eng')[:shutdown]).to eq(false) + end + end + + describe '#set_send_community' do + it 'enable neighbor send community' do + expect(subject.get('eng')[:send_community]).to eq(false) + expect(subject.set_send_community('eng')).to be_truthy + expect(subject.get('eng')[:send_community]).to eq(true) + end + + it 'negate neighbor send community' do + expect(subject.get('eng')[:send_community]).to eq(true) + expect(subject.set_send_community('eng', enable: false)).to be_truthy + expect(subject.get('eng')[:send_community]).to eq(false) + end + + it 'default neighbor send community' do + expect(subject.set_send_community('eng')).to be_truthy + expect(subject.get('eng')[:send_community]).to eq(true) + expect(subject.set_send_community('eng', default: true)).to be_truthy + expect(subject.get('eng')[:send_community]).to eq(false) + end + end + + describe '#set_next_hop_self' do + it 'enable neighbor next hop self' do + expect(subject.get('eng')[:next_hop_self]).to eq(false) + expect(subject.set_next_hop_self('eng')).to be_truthy + expect(subject.get('eng')[:next_hop_self]).to eq(true) + end + + it 'negate neighbor next hop self' do + expect(subject.get('eng')[:next_hop_self]).to eq(true) + expect(subject.set_next_hop_self('eng', enable: false)).to be_truthy + expect(subject.get('eng')[:next_hop_self]).to eq(false) + end + + it 'default neighbor next hop self' do + expect(subject.set_next_hop_self('eng')).to be_truthy + expect(subject.get('eng')[:next_hop_self]).to eq(true) + expect(subject.set_next_hop_self('eng', default: true)).to be_truthy + expect(subject.get('eng')[:next_hop_self]).to eq(false) + end + end + + describe '#set_route_map_in' do + it 'set route map in value' do + expect(subject.get('eng')[:route_map_in]).to eq(nil) + expect(subject.set_route_map_in('eng', value: 'edge')).to be_truthy + expect(subject.get('eng')[:route_map_in]).to eq('edge') + end + + it 'negate route map in value' do + expect(subject.get('eng')[:route_map_in]).to eq('edge') + expect(subject.set_route_map_in('eng', value: 'edge', enable: false)) + .to be_truthy + expect(subject.get('eng')[:route_map_in]).to eq(nil) + end + + it 'default route map in value' do + expect(subject.set_route_map_in('eng', value: 'edge')).to be_truthy + expect(subject.get('eng')[:route_map_in]).to eq('edge') + expect(subject.set_route_map_in('eng', value: 'edge', default: true)) + .to be_truthy + expect(subject.get('eng')[:route_map_in]).to eq(nil) + end + end + + describe '#set_route_map_out' do + it 'set route map out value' do + expect(subject.get('eng')[:route_map_out]).to eq(nil) + expect(subject.set_route_map_out('eng', value: 'edge')).to be_truthy + expect(subject.get('eng')[:route_map_out]).to eq('edge') + end + + it 'negate route map out value' do + expect(subject.get('eng')[:route_map_out]).to eq('edge') + expect(subject.set_route_map_out('eng', value: 'edge', enable: false)) + .to be_truthy + expect(subject.get('eng')[:route_map_out]).to eq(nil) + end + + it 'default route map out value' do + expect(subject.set_route_map_out('eng', value: 'edge')).to be_truthy + expect(subject.get('eng')[:route_map_out]).to eq('edge') + expect(subject.set_route_map_out('eng', value: 'edge', default: true)) + .to be_truthy + expect(subject.get('eng')[:route_map_out]).to eq(nil) + end + end + + describe '#set_description' do + it 'set the description value' do + expect(subject.get('eng')[:description]).to eq(nil) + expect(subject.set_description('eng', value: 'text')).to be_truthy + expect(subject.get('eng')[:description]).to eq('text') + end + + it 'negate the description value' do + expect(subject.get('eng')[:description]).to eq('text') + expect(subject.set_description('eng', enable: false)).to be_truthy + expect(subject.get('eng')[:description]).to eq(nil) + end + + it 'defaults the description value' do + expect(subject.set_description('eng', value: 'text')).to be_truthy + expect(subject.get('eng')[:description]).to eq('text') + expect(subject.set_description('eng', default: true)).to be_truthy + expect(subject.get('eng')[:description]).to eq(nil) + end + end +end diff --git a/spec/system/rbeapi/api/bgp_spec.rb b/spec/system/rbeapi/api/bgp_spec.rb new file mode 100644 index 0000000..d04078a --- /dev/null +++ b/spec/system/rbeapi/api/bgp_spec.rb @@ -0,0 +1,275 @@ +# +# 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. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/bgp' + +describe Rbeapi::Api::Bgp do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + let(:test) do + { bgp_as: '64600', + router_id: '192.168.254.1', + shutdown: false, + maximum_paths: 32, + maximum_ecmp_paths: 32, + networks: [ + { prefix: '192.168.254.1', masklen: 32, route_map: nil }, + { prefix: '192.168.254.2', masklen: 32, route_map: 'rmap' } + ], + neighbors: { + 'eBGP_GROUP' => { + peer_group: nil, remote_as: nil, send_community: false, + shutdown: false, description: nil, next_hop_self: false, + route_map_in: nil, route_map_out: nil + }, + '192.168.255.1' => { + peer_group: 'eBGP_GROUP', remote_as: '65000', send_community: true, + shutdown: true, description: nil, next_hop_self: true, + route_map_in: nil, route_map_out: nil + }, + '192.168.255.3' => { + peer_group: 'eBGP_GROUP', remote_as: '65001', send_community: true, + shutdown: true, description: nil, next_hop_self: true, + route_map_in: nil, route_map_out: nil + } + } + } + end + + let(:response) do + { + bgp_as: '1000', + router_id: nil, + shutdown: false, + maximum_paths: 1, + maximum_ecmp_paths: 128, + networks: [], + neighbors: {} + } + end + + describe '#get' do + before do + node.config(['no router bgp 64600', 'router bgp 64600', + 'router-id 192.168.254.1', 'maximum-paths 32 ecmp 32', + 'neighbor 192.168.255.1 peer-group eBGP_GROUP', + 'neighbor 192.168.255.1 remote-as 65000', + 'neighbor 192.168.255.3 peer-group eBGP_GROUP', + 'neighbor 192.168.255.3 remote-as 65001', + 'network 192.168.254.1/32', + 'network 192.168.254.2/32 route-map rmap', + 'aggregate-address 192.168.255.0/28 summary-only']) + end + + it 'returns the BGP resource' do + expect(subject.get).to eq(test) + end + end + + describe '#create' do + before do + node.config(['no router bgp 64600']) + end + + it 'create a new BGP resource' do + expect(subject.get).to eq(nil) + expect(subject.create('1000')).to be_truthy + expect(subject.get).to eq(response) + end + + it 'create with enable' do + expect(subject.get).to eq(nil) + expect(subject.create('1000', enable: true)).to be_truthy + expect(subject.get).to eq(response) + end + + it 'create with router_id' do + expect(subject.get).to eq(nil) + expect(subject.create('1000', router_id: '1.2.3.4')).to be_truthy + expect(subject.get[:router_id]).to eq('1.2.3.4') + end + + it 'create with maximum paths' do + expect(subject.get).to eq(nil) + expect(subject.create('1000', maximum_paths: 3)).to be_truthy + expect(subject.get[:maximum_paths]).to eq(3) + end + + it 'create with maximum paths and ecmp paths' do + expect(subject.get).to eq(nil) + expect(subject.create('1000', maximum_paths: 13, + maximum_ecmp_paths: 13)).to be_truthy + expect(subject.get[:maximum_paths]).to eq(13) + expect(subject.get[:maximum_ecmp_paths]).to eq(13) + end + + it 'raises ArgumentError for create with ecmp paths only' do + expect { subject.create('1000', maximum_ecmp_paths: 13) }.to \ + raise_error ArgumentError + end + + it 'create with all options set' do + expect(subject.create('1000', enable: true, router_id: '1.2.3.4', + maximum_paths: 13, + maximum_ecmp_paths: 13)).to be_truthy + expect(subject.get[:router_id]).to eq('1.2.3.4') + expect(subject.get[:maximum_paths]).to eq(13) + expect(subject.get[:maximum_ecmp_paths]).to eq(13) + end + end + + describe '#delete' do + before do + node.config(['no router bgp 1000', 'router bgp 1000']) + end + + it 'delete a BGP resource' do + expect(subject.get).to eq(response) + expect(subject.delete).to be_truthy + expect(subject.get).to eq(nil) + end + end + + describe '#default' do + before do + node.config(['no router bgp 1000', 'router bgp 1000', + 'maximum-paths 13 ecmp 15']) + end + + let(:before) do + { + bgp_as: '1000', + router_id: nil, + shutdown: false, + maximum_paths: 13, + maximum_ecmp_paths: 15, + networks: [], + neighbors: {} + } + end + + it 'sets router to default value' do + expect(subject.get).to eq(before) + expect(subject.default).to be_truthy + expect(subject.get).to eq(nil) + end + end + + describe '#set_router_id' do + before do + node.config(['no router bgp 1000', 'router bgp 1000']) + end + + it 'set the router id' do + expect(subject.set_router_id(value: '1.2.3.4')).to be_truthy + expect(subject.get[:router_id]).to eq('1.2.3.4') + end + + it 'remove the router-id without a value' do + expect(subject.set_router_id(enable: false)).to be_truthy + expect(subject.get[:router_id]).to eq(nil) + end + + it 'remove the router-id with a value' do + expect(subject.set_router_id(value: '1.2.3.4', enable: false)) + .to be_truthy + expect(subject.get[:router_id]).to eq(nil) + end + + it 'defaults the router-id without a value' do + expect(subject.set_router_id(value: '1.2.3.4')).to be_truthy + expect(subject.set_router_id(default: true)).to be_truthy + expect(subject.get[:router_id]).to eq(nil) + end + + it 'defaults the router-id with a value' do + expect(subject.set_router_id(value: '1.2.3.4', default: true)) + .to be_truthy + expect(subject.get[:router_id]).to eq(nil) + end + end + + describe '#set_shutdown' do + it 'enable BGP routing process' do + expect(subject.set_shutdown(enable: true)).to be_truthy + end + + it 'disable BGP routing process' do + expect(subject.set_shutdown(enable: false)).to be_truthy + end + + it 'default BGP routing process state' do + expect(subject.set_shutdown(default: true)).to be_truthy + end + end + + describe '#set_maximum_paths' do + it 'set the maximum paths and ecmp paths' do + expect(subject.set_maximum_paths(13, 16)).to be_truthy + end + + it 'remove the maximum paths' do + expect(subject.set_maximum_paths(0, 0, enable: false)).to be_truthy + end + + it 'defaults the maximum paths' do + expect(subject.set_maximum_paths(0, 0, default: true)).to be_truthy + end + end + + describe '#add_network' do + it 'add a BGP network with a route map' do + expect(subject.add_network('1.2.3.0', 24, 'eng')).to be_truthy + end + + it 'add a BGP network without a route map' do + expect(subject.add_network('1.2.3.0', 24)).to be_truthy + end + end + + describe '#remove_network' do + it 'remove a BGP network with a route map' do + expect(subject.remove_network('1.2.3.0', 24, 'eng')).to be_truthy + end + + it 'remove a BGP network without a route map' do + expect(subject.remove_network('1.2.3.0', 24)).to be_truthy + end + end +end diff --git a/spec/system/rbeapi/api/dns_spec.rb b/spec/system/rbeapi/api/dns_spec.rb index eb683cb..9a2201a 100644 --- a/spec/system/rbeapi/api/dns_spec.rb +++ b/spec/system/rbeapi/api/dns_spec.rb @@ -114,11 +114,27 @@ expect(subject.get[:domain_list]).to be_empty end - it 'default the name servers list' do + it 'default true the name servers list' do expect(subject.get[:domain_list]).to be_empty + expect(subject.set_domain_list(value: servers)).to be_truthy expect(subject.set_domain_list(default: true)).to be_truthy expect(subject.get[:domain_list]).to be_empty end + + it 'default false the name servers list' do + expect(subject.get[:domain_list]).to be_empty + expect(subject.set_domain_list(default: false, + value: servers)).to be_truthy + expect(subject.get[:domain_list]).to eq(servers) + end + + it 'default the name servers list with previous values' do + expect(subject.get[:domain_list]).to be_empty + expect(subject.set_domain_list(value: %w(foob bat))).to be_truthy + expect(subject.set_domain_list(default: false, + value: servers)).to be_truthy + expect(subject.get[:domain_list]).to eq(servers) + end end describe '#add_domain_list' do diff --git a/spec/system/rbeapi/api/interfaces_base_spec.rb b/spec/system/rbeapi/api/interfaces_base_spec.rb index e81b94f..ae27da1 100644 --- a/spec/system/rbeapi/api/interfaces_base_spec.rb +++ b/spec/system/rbeapi/api/interfaces_base_spec.rb @@ -11,15 +11,56 @@ Rbeapi::Client.connect_to('dut') end + describe '#respond_to?' do + it 'test to validate endpoint' do + expect(subject.respond_to?('get', 'Ethernet1')).to be_truthy + end + end + describe '#get' do - let(:entity) do - { name: 'Loopback0', type: 'generic', description: '', shutdown: false } + context 'with interface Loopback' do + let(:entity) do + { name: 'Loopback0', type: 'generic', description: '', shutdown: false } + end + + before { node.config(['no interface Loopback0', 'interface Loopback0']) } + + it 'returns the interface resource' do + expect(subject.get('Loopback0')).to eq(entity) + end end - before { node.config(['no interface Loopback0', 'interface Loopback0']) } + context 'with interface Port-Channel' do + let(:entity) do + { name: 'Port-Channel1', type: 'portchannel', description: '', + shutdown: false, members: [], lacp_mode: 'on', minimum_links: '0', + lacp_fallback: 'disabled', lacp_timeout: '90' } + end + + before do + node.config(['no interface Loopback0', 'no interface Port-Channel1', + 'interface Port-Channel1']) + end + + it 'returns the interface resource' do + expect(subject.get('Port-Channel1')).to eq(entity) + end + end + + context 'with interface Vxlan' do + let(:entity) do + { name: 'Vxlan1', type: 'vxlan', description: '', + shutdown: false, source_interface: '', multicast_group: '', + udp_port: 4789, flood_list: [], vlans: {} } + end + + before do + node.config(['no interface Vxlan1', 'interface Vxlan1']) + end - it 'returns the interface resource' do - expect(subject.get('Loopback0')).to eq(entity) + it 'returns the interface resource' do + expect(subject.get('Vxlan1')).to eq(entity) + end end end diff --git a/spec/system/rbeapi/api/interfaces_ethernet_spec.rb b/spec/system/rbeapi/api/interfaces_ethernet_spec.rb index d8b5c5a..1940ef4 100644 --- a/spec/system/rbeapi/api/interfaces_ethernet_spec.rb +++ b/spec/system/rbeapi/api/interfaces_ethernet_spec.rb @@ -1,3 +1,4 @@ + require 'spec_helper' require 'rbeapi/client' @@ -85,6 +86,19 @@ end end + describe '#set_speed' do + before { node.config(['default interface Ethernet1']) } + + it 'sets default true' do + expect(subject.set_speed('Ethernet1', default: true)).to be_truthy + end + + it 'sets enable true' do + expect(subject.set_speed('Ethernet1', default: false, + enable: true)).to be_falsy + end + end + describe '#set_sflow' do it 'sets the sflow value to true' do node.config(['interface Ethernet1', 'no sflow enable']) diff --git a/spec/system/rbeapi/api/interfaces_portchannel_spec.rb b/spec/system/rbeapi/api/interfaces_portchannel_spec.rb index 8afd324..6814e98 100644 --- a/spec/system/rbeapi/api/interfaces_portchannel_spec.rb +++ b/spec/system/rbeapi/api/interfaces_portchannel_spec.rb @@ -134,6 +134,74 @@ expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 Ethernet3)) end + + it 'updates the member interfaces and mode on existing interface' do + node.config(['no interface Port-Channel1', 'interface Ethernet1-2', + 'channel-group 1 mode on']) + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('on') + expect(subject.set_members('Port-Channel1', + %w(Ethernet1 Ethernet3), + 'active')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet3)) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('active') + end + end + + describe '#add_member' do + before do + node.config(['no interface Port-Channel1', + 'interface Port-Channel1']) + end + + it 'adds new members to the port-channel interface' do + node.config(['no interface Port-Channel1', 'interface Port-Channel1']) + expect(subject.get('Port-Channel1')[:members]).not_to include('Ethernet1') + expect(subject.add_member('Port-Channel1', 'Ethernet1')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(['Ethernet1']) + end + + it 'updates the member interfaces on existing interface' do + node.config(['no interface Port-Channel1', 'interface Ethernet1-2', + 'channel-group 1 mode on']) + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.add_member('Port-Channel1', 'Ethernet3')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2 + Ethernet3)) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('on') + end + + it 'no update to the member interfaces on existing interface' do + node.config(['no interface Port-Channel1', 'interface Ethernet1-2', + 'channel-group 1 mode active']) + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.add_member('Port-Channel1', 'Ethernet2')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('active') + end + end + + describe '#remove_member' do + before do + node.config(['no interface Port-Channel1', + 'interface Port-Channel1']) + end + + it 'removes the member interface on existing interface' do + node.config(['no interface Port-Channel1', 'interface Ethernet1-2', + 'channel-group 1 mode on']) + expect(subject.get('Port-Channel1')[:members]).to eq(%w(Ethernet1 + Ethernet2)) + expect(subject.remove_member('Port-Channel1', 'Ethernet1')).to be_truthy + expect(subject.get('Port-Channel1')[:members]).to eq(['Ethernet2']) + expect(subject.get('Port-Channel1')[:lacp_mode]).to eq('on') + end end describe '#set_lacp_mode' do diff --git a/spec/system/rbeapi/api/interfaces_vxlan_spec.rb b/spec/system/rbeapi/api/interfaces_vxlan_spec.rb index e40ea6c..89f91e0 100644 --- a/spec/system/rbeapi/api/interfaces_vxlan_spec.rb +++ b/spec/system/rbeapi/api/interfaces_vxlan_spec.rb @@ -41,7 +41,6 @@ before { node.config('no interface Vxlan1') } it 'creates a new interface resource' do - expect(subject.get('Vxlan1')).to be_nil expect(subject.create('Vxlan1')).to be_truthy expect(subject.get('Vxlan1')).not_to be_nil end diff --git a/spec/system/api_ospf_interfaces_spec.rb b/spec/system/rbeapi/api/ospf_interfaces_spec.rb similarity index 95% rename from spec/system/api_ospf_interfaces_spec.rb rename to spec/system/rbeapi/api/ospf_interfaces_spec.rb index 3b1b22a..66f479a 100644 --- a/spec/system/api_ospf_interfaces_spec.rb +++ b/spec/system/rbeapi/api/ospf_interfaces_spec.rb @@ -12,8 +12,9 @@ describe '#get' do before do node.config(['default interface Ethernet1', 'interface Ethernet1', - 'no switchport', 'ip address 88.99.99.99/24', 'exit', - 'default interface Ethernet2']) + 'no switchport', 'ip address 88.99.99.99/24', + 'ip ospf network point-to-point', + 'exit', 'default interface Ethernet2']) end it 'returns an ospf interface resource instance' do diff --git a/spec/system/api_ospf_spec.rb b/spec/system/rbeapi/api/ospf_spec.rb similarity index 88% rename from spec/system/api_ospf_spec.rb rename to spec/system/rbeapi/api/ospf_spec.rb index ff66d2c..22c95d1 100644 --- a/spec/system/api_ospf_spec.rb +++ b/spec/system/rbeapi/api/ospf_spec.rb @@ -10,10 +10,19 @@ let(:node) { Rbeapi::Client.connect_to('dut') } describe '#get' do - before { node.config(['no router ospf 1', 'router ospf 1']) } + before do + node.config(['no router ospf 1', + 'router ospf 1', + 'router-id 1.1.1.1', + 'redistribute static route-map word', + 'network 192.168.10.10/24 area 0.0.0.0', + 'network 192.168.11.10/24 area 0.0.0.0']) + end let(:entity) do - { 'router_id' => '', 'areas' => {}, 'redistribute' => {} } + { 'router_id' => '1.1.1.1', + 'areas' => { '0.0.0.0' => ['192.168.10.0/24', '192.168.11.0/24'] }, + 'redistribute' => { 'static' => { 'route_map' => 'word' } } } end it 'returns an ospf resource instance' do diff --git a/spec/system/rbeapi/api/routemaps_spec.rb b/spec/system/rbeapi/api/routemaps_spec.rb index 4b6edca..ee3cff4 100644 --- a/spec/system/rbeapi/api/routemaps_spec.rb +++ b/spec/system/rbeapi/api/routemaps_spec.rb @@ -15,8 +15,7 @@ let(:resource) { subject.get } before do - node.config(['no route-map test', 'no route-map test1', - 'no route-map test2', 'no route-map test3', + node.config(['no route-map test', 'route-map test permit 10', 'route-map test permit 20', 'description descript', 'match ip address prefix-list MYLOOPBACK', @@ -90,8 +89,8 @@ expect(subject.getall).to be_a_kind_of(Hash) end - it 'has a key for description' do - expect(subject.getall.count).to eq(2) + it 'has at least two entries' do + expect(subject.getall.count).to be >= 2 end it 'returns the routemap collection' do diff --git a/spec/system/rbeapi/api/snmp_spec.rb b/spec/system/rbeapi/api/snmp_spec.rb index 43763a3..7184db6 100644 --- a/spec/system/rbeapi/api/snmp_spec.rb +++ b/spec/system/rbeapi/api/snmp_spec.rb @@ -23,6 +23,30 @@ end end + describe '#set_notification' do + before { node.config(['default snmp-server']) } + + it 'configures the snmp notification value to on' do + expect(subject.set_notification(state: 'on', + name: 'bgp')).to be_truthy + expect(subject.get[:notifications][0]).to eq(name: 'bgp', + state: 'on') + end + + it 'configures the snmp notification value to off' do + expect(subject.set_notification(state: 'off', + name: 'bgp')).to be_truthy + expect(subject.get[:notifications][0]).to eq(name: 'bgp', + state: 'off') + end + + it 'configures the snmp notification value to default' do + expect(subject.set_notification(state: 'default', + name: 'all')).to be_truthy + expect(subject.get).to include(:notifications) + end + end + describe '#set_location' do before { node.config(['no snmp-server location']) } @@ -119,6 +143,47 @@ end end + describe '#add_community' do + before { node.config('no snmp-server community foo') } + + it 'adds the specified community' do + expect(subject.add_community('foo')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('ro') + end + + it 'adds the specified community ro' do + expect(subject.add_community('foo', 'ro')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('ro') + end + + it 'adds the specified community rw' do + expect(subject.add_community('foo', 'rw')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('rw') + end + end + + describe '#remove_community' do + before { node.config('default snmp-server community foo') } + + it 'removes the specified community foo' do + expect(subject.remove_community('foo')).to be_truthy + end + end + + describe '#set_community_access' do + before { node.config('default snmp-server community foo') } + + it 'sets the community access to ro' do + expect(subject.set_community_access('foo', 'ro')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('ro') + end + + it 'sets the community access to rw' do + expect(subject.set_community_access('foo', 'rw')).to be_truthy + expect(subject.get[:communities]['foo'][:access]).to eq('rw') + end + end + describe '#set_community_acl' do before do node.config(['no snmp-server community foo', diff --git a/spec/system/rbeapi/api/staticroutes_spec.rb b/spec/system/rbeapi/api/staticroutes_spec.rb new file mode 100644 index 0000000..a4980e2 --- /dev/null +++ b/spec/system/rbeapi/api/staticroutes_spec.rb @@ -0,0 +1,177 @@ +# +# 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. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/staticroutes' + +describe Rbeapi::Api::Staticroutes do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + describe '#getall' do + let(:resource) { subject.getall } + + before do + node.config(['no ip route 1.2.3.4/32', + 'no ip route 192.0.2.0/24', + 'no ip route 192.0.3.0/24', + 'ip route 1.2.3.4/32 Ethernet7 4 tag 3 name frank', + 'ip route 1.2.3.4/32 Null0 32 tag 3 name fred', + 'ip route 192.0.2.0/24 Ethernet7 3 tag 0 name dummy1', + 'ip route 192.0.3.0/24 192.0.3.1 1 tag 0 name dummy2']) + end + + it 'returns the staticroute collection' do + expect(subject.getall).to include(destination: '1.2.3.4/32', + nexthop: 'Ethernet7', + distance: '4', + tag: '3', + name: 'frank') + expect(subject.getall).to include(destination: '1.2.3.4/32', + nexthop: 'Null0', + distance: '32', + tag: '3', + name: 'fred') + expect(subject.getall).to include(destination: '192.0.2.0/24', + nexthop: 'Ethernet7', + distance: '3', + tag: '0', + name: 'dummy1') + expect(subject.getall).to include(destination: '192.0.3.0/24', + nexthop: '192.0.3.1', + distance: '1', + tag: '0', + name: 'dummy2') + end + + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Array) + end + + it 'has four entries' do + expect(subject.getall.size).to eq(4) + end + end + + describe '#create' do + let(:resource) { subject.getall } + + before(:each) do + node.config(['no ip route 1.2.3.4/32', + 'no ip route 192.0.2.0/24', + 'no ip route 192.0.3.0/24']) + end + + context 'creates a new staticroute resoure' do + it 'with minimum input' do + expect(subject.getall).to eq([]) + expect(subject.create('192.0.2.0/24', 'Ethernet1')).to be_truthy + expect(subject.getall).to eq(resource) + end + + it 'with a router_ip' do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + expect(subject.getall).to eq(resource) + + expect(subject.create('192.0.2.0/24', 'Ethernet1', + router_ip: '192.168.1.1')).to be_truthy + expect(subject.getall).to eq(resource) + end + + it 'with distance (metric)' do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + expect(subject.getall).to eq(resource) + + expect(subject.create('192.0.2.0/24', 'Ethernet1', distance: 254)) + .to be_truthy + expect(subject.getall).to include(destination: '192.0.2.0/24', + nexthop: 'Ethernet1', + distance: '254', + tag: '0', + name: nil) + end + + it 'with a tag' do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + expect(subject.getall).to eq(resource) + + expect(subject.create('192.0.2.0/24', 'Ethernet1', tag: 3)) + .to be_truthy + expect(subject.getall).to include(destination: '192.0.2.0/24', + nexthop: 'Ethernet1', + distance: '1', + tag: '3', + name: nil) + end + + it 'with a name' do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + expect(subject.getall).to eq(resource) + + expect(subject.create('192.0.2.0/24', 'Ethernet1', name: 'my_route')) + .to be_truthy + expect(subject.getall).to include(destination: '192.0.2.0/24', + nexthop: 'Ethernet1', + distance: '1', + tag: '0', + name: 'my_route') + end + end + end + + describe '#delete' do + let(:resource) { subject.getall } + + before do + node.config(['ip route 192.0.2.0/24 Ethernet1']) + end + + context 'deletes a staticroute resource' do + it 'given only a destination network' do + expect(subject.getall).to eq(resource) + expect(subject.delete('192.0.2.0/24')).to be_truthy + expect(subject.getall).to eq([]) + end + + it 'given a destination and nexthop' do + expect(subject.getall).to eq(resource) + expect(subject.delete('192.0.2.0/24', 'Ethernet1')).to be_truthy + expect(subject.getall).to eq([]) + end + end + end +end diff --git a/spec/system/rbeapi/api/stp_instances_spec.rb b/spec/system/rbeapi/api/stp_instances_spec.rb index 80b47bf..3170751 100644 --- a/spec/system/rbeapi/api/stp_instances_spec.rb +++ b/spec/system/rbeapi/api/stp_instances_spec.rb @@ -65,5 +65,25 @@ expect(subject.set_priority('1', value: '4096')).to be_truthy expect(subject.get('1')[:priority]).to eq('4096') end + + it 'set the instance priority to default' do + expect(subject.set_priority('1', value: '4096', + default: true)).to be_truthy + expect(subject.get('1')[:priority]).to eq('32768') + end + + it 'set the instance priority to enable false' do + expect(subject.set_priority('1', value: '4096', + default: false, + enable: false)).to be_truthy + expect(subject.get('1')[:priority]).to eq('32768') + end + + it 'set the instance priority to enable true' do + expect(subject.set_priority('1', value: '4096', + default: false, + enable: true)).to be_truthy + expect(subject.get('1')[:priority]).to eq('4096') + end end end diff --git a/spec/system/rbeapi/api/stp_interfaces_spec.rb b/spec/system/rbeapi/api/stp_interfaces_spec.rb index 471c268..ac770ac 100644 --- a/spec/system/rbeapi/api/stp_interfaces_spec.rb +++ b/spec/system/rbeapi/api/stp_interfaces_spec.rb @@ -89,6 +89,13 @@ end describe '#set_bpduguard' do + it 'sets the bpduguard value to default true' do + node.config(['interface Ethernet1', 'no spanning-tree bpduguard']) + expect(subject.get('Ethernet1')[:bpduguard]).to be_falsy + expect(subject.set_bpduguard('Ethernet1', default: true)).to be_truthy + expect(subject.get('Ethernet1')[:bpduguard]).to be_falsey + end + it 'sets the bpduguard value to true' do node.config(['interface Ethernet1', 'no spanning-tree bpduguard']) expect(subject.get('Ethernet1')[:bpduguard]).to be_falsy diff --git a/spec/system/rbeapi/api/switchports_spec.rb b/spec/system/rbeapi/api/switchports_spec.rb index dff9316..65740d0 100644 --- a/spec/system/rbeapi/api/switchports_spec.rb +++ b/spec/system/rbeapi/api/switchports_spec.rb @@ -13,29 +13,44 @@ describe '#get' do let(:keys) do - [:mode, :access_vlan, :trunk_native_vlan, :trunk_allowed_vlans] + [:mode, :access_vlan, :trunk_native_vlan, :trunk_allowed_vlans, + :trunk_groups] end - before do - node.config(['default interface Ethernet1', 'interface Ethernet2', - 'no switchport']) - end + context 'vlan as an integer range' do + before do + node.config(['default interface Ethernet1', 'interface Ethernet2', + 'no switchport']) + end - it 'returns the switchport resource' do - expect(subject.get('Ethernet1')).not_to be_nil - end + it 'returns the switchport resource' do + expect(subject.get('Ethernet1')).not_to be_nil + end - it 'does not return a nonswitchport resource' do - expect(subject.get('Ethernet2')).to be_nil - end + it 'does not return a nonswitchport resource' do + expect(subject.get('Ethernet2')).to be_nil + end + + it 'has all required keys' do + expect(subject.get('Ethernet1').keys).to eq(keys) + end - it 'has all required keys' do - expect(subject.get('Ethernet1').keys).to eq(keys) + it 'returns allowed_vlans as an array' do + expect(subject.get('Ethernet1')[:trunk_allowed_vlans]) + .to be_a_kind_of(Array) + end end - it 'returns allowed_vlans as an array' do - expect(subject.get('Ethernet1')[:trunk_allowed_vlans]) - .to be_a_kind_of(Array) + context 'vlan as an integer' do + before do + node.config(['default interface Ethernet1', + 'interface Ethernet1', + 'switchport trunk allowed vlan 1']) + end + + it 'returns the switchport resource' do + expect(subject.get('Ethernet1')).not_to be_nil + end end end @@ -206,4 +221,59 @@ expect(subject.get('Ethernet1')[:trunk_allowed_vlans].length).to eq(4094) end end + + describe '#set_trunk_groups' do + before do + node.config(['interface Ethernet1', 'default switchport trunk group']) + end + + it 'raises an ArgumentError if value is not an array' do + expect { subject.set_trunk_groups('Ethernet1', value: 'foo') } + .to raise_error(ArgumentError) + end + + it 'sets trunk group to foo bar bang' do + node.config(['interface Ethernet1', 'switchport trunk group bang', + 'switchport trunk group baz']) + expect(subject.get('Ethernet1')[:trunk_groups]).to eq(%w(bang baz)) + expect(subject.set_trunk_groups('Ethernet1', value: %w(foo bar bang))) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups].sort) + .to eq(%w(bang bar foo)) + end + + it 'clears trunk group if no value specified' do + node.config(['interface Ethernet1', 'switchport trunk group bang', + 'switchport trunk group baz']) + expect(subject.get('Ethernet1')[:trunk_groups]).to eq(%w(bang baz)) + expect(subject.set_trunk_groups('Ethernet1')).to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups]).to be_empty + end + + it 'negate switchport trunk group' do + node.config(['interface Ethernet1', 'switchport trunk group bang', + 'switchport trunk group baz']) + expect(subject.get('Ethernet1')[:trunk_groups]).to eq(%w(bang baz)) + expect(subject.set_trunk_groups('Ethernet1', value: %w(foo bar bang))) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups].sort) + .to eq(%w(bang bar foo)) + expect(subject.set_trunk_groups('Ethernet1', enable: false)) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups]).to be_empty + end + + it 'default switchport trunk group' do + node.config(['interface Ethernet1', 'switchport trunk group bang', + 'switchport trunk group baz']) + expect(subject.get('Ethernet1')[:trunk_groups]).to eq(%w(bang baz)) + expect(subject.set_trunk_groups('Ethernet1', value: %w(foo bar bang))) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups].sort) + .to eq(%w(bang bar foo)) + expect(subject.set_trunk_groups('Ethernet1', default: true)) + .to be_truthy + expect(subject.get('Ethernet1')[:trunk_groups]).to be_empty + end + end end diff --git a/spec/system/rbeapi/api/users_spec.rb b/spec/system/rbeapi/api/users_spec.rb new file mode 100644 index 0000000..2623cce --- /dev/null +++ b/spec/system/rbeapi/api/users_spec.rb @@ -0,0 +1,324 @@ +# +# 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. +# +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/users' + +describe Rbeapi::Api::Users do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + let(:sshkey) do + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDKL1UtBALa4CvFUsHUipN' \ + 'ymA04qCXuAtTwNcMj84bTUzUI+q7mdzRCTLkllXeVxKuBnaTm2PW7W67K5C' \ + 'Vpl0EVCm6IY7FS7kc4nlnD/tFvTvShy/fzYQRAdM7ZfVtegW8sMSFJzBR/T' \ + '/Y/sxI16Y/dQb8fC3la9T25XOrzsFrQiKRZmJGwg8d+0RLxpfMg0s/9ATwQ' \ + 'Kp6tPoLE4f3dKlAgSk5eENyVLA3RsypWADHpenHPcB7sa8D38e1TS+n+EUy' \ + 'Adb3Yov+5ESAbgLIJLd52Xv+FyYi0c2L49ByBjcRrupp4zfXn4DNRnEG4K6' \ + 'GcmswHuMEGZv5vjJ9OYaaaaaaa' + end + + let(:secret) do + '$6$RMxgK5ALGIf.nWEC$tHuKCyfNtJMCY561P52dTzHUmYMmLxb/M' \ + 'xik.j3vMUs8lMCPocM00/NAS.SN6GCWx7d/vQIgxnClyQLAb7n3x0' + end + + let(:md5_secret) do + '$1$Ehb5lL0D$N3MgrkfMFxmeh0FSZ5sEZ1' + end + + let(:test) do + { name: 'rbeapi', + privilege: 1, + role: nil, + nopassword: false, + encryption: 'md5', + secret: md5_secret, + sshkey: sshkey + } + end + + describe '#getall' do + let(:resource) { subject.getall } + + let(:test1_entries) do + { 'admin' => { name: 'admin', privilege: 1, + role: 'network-admin', nopassword: true, + encryption: nil, secret: nil, sshkey: nil }, + 'rbeapi' => { name: 'rbeapi', privilege: 1, role: nil, + nopassword: false, encryption: 'md5', + secret: md5_secret, + sshkey: sshkey } + } + end + + before do + node.config(['no username rbeapi', + 'no username user1', + 'username admin privilege 1 role network-admin nopassword', + "username rbeapi privilege 1 secret 5 #{md5_secret}", + "username rbeapi sshkey #{sshkey}"]) + end + + it 'returns the username collection' do + expect(subject.getall).to include(test1_entries) + end + + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Hash) + end + end + + describe '#get' do + it 'returns the user resource for given name' do + expect(subject.get('rbeapi')).to eq(test) + end + + it 'returns a hash' do + expect(subject.get('rbeapi')).to be_a_kind_of(Hash) + end + + it 'has two entries' do + expect(subject.get('rbeapi').size).to eq(7) + end + end + + describe '#create' do + before do + node.config(['no username rbeapi']) + end + + it 'create a new user name with no password' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', nopassword: :true)).to be_truthy + expect(subject.get('rbeapi')[:nopassword]).to eq(true) + end + + it 'create a new user name with no password and privilege' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + privilege: 4, + nopassword: :true)).to be_truthy + expect(subject.get('rbeapi')[:privilege]).to eq(4) + end + + it 'create a new user name with no password, privilege, and role' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + privilege: 4, + role: 'net-minion', + nopassword: :true)).to be_truthy + expect(subject.get('rbeapi')[:privilege]).to eq(4) + expect(subject.get('rbeapi')[:role]).to eq('net-minion') + expect(subject.get('rbeapi')[:nopassword]).to eq(true) + end + + it 'create a new user name with a password' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', secret: 'icanttellyou')).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('md5') + end + + it 'create a new user name with a password and privilege' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: 'icanttellyou', + privilege: 5)).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('md5') + expect(subject.get('rbeapi')[:privilege]).to eq(5) + end + + it 'create a new user name with a password, privilege, and role' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: 'icanttellyou', + privilege: 5, role: 'net')).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('md5') + expect(subject.get('rbeapi')[:privilege]).to eq(5) + expect(subject.get('rbeapi')[:role]).to eq('net') + end + + it 'create a new user name with a password and md5 encryption' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: '$1$Wb4zN5EH$ILNgYb3Ehzs85S9KpoFW4.', + encryption: 'md5')).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('md5') + expect(subject.get('rbeapi')[:secret]) + .to eq('$1$Wb4zN5EH$ILNgYb3Ehzs85S9KpoFW4.') + end + + it 'create a new user name with a password and sha512 encryption' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: secret, + encryption: 'sha512')).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('sha512') + end + + it 'create a new user name with a password, sha512 encryption, and key' do + expect(subject.get('rbeapi')).to eq(nil) + expect(subject.create('rbeapi', + secret: secret, + encryption: 'sha512', + sshkey: sshkey)).to be_truthy + expect(subject.get('rbeapi')[:encryption]).to eq('sha512') + end + + it 'raises ArgumentError for create without required args ' do + expect { subject.create('rbeapi') }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for invalid encryption value' do + expect { subject.create('name', encryption: 'bogus') }.to \ + raise_error ArgumentError + end + end + + describe '#delete' do + before do + node.config(['username user1 privilege 1 role network-admin nopassword']) + end + + it 'delete a username resource' do + expect(subject.get('user1')[:name]).to eq('user1') + expect(subject.delete('user1')).to be_truthy + expect(subject.get('user1')).to eq(nil) + end + end + + describe '#default' do + before do + node.config(['username user1 privilege 1 role network-admin nopassword']) + end + + it 'sets username resource to default value' do + expect(subject.get('user1')[:name]).to eq('user1') + expect(subject.default('user1')).to be_truthy + expect(subject.get('user1')).to eq(nil) + end + end + + describe '#set_privilege' do + before do + node.config(['no username rbeapi', + 'username rbeapi role network-admin nopassword']) + end + + it 'set the privilege' do + expect(subject.set_privilege('rbeapi', value: '13')).to be_truthy + expect(subject.get('rbeapi')[:privilege]).to eq(13) + end + + it 'remove the privilege without a value' do + expect(subject.set_privilege('rbeapi', enable: false)).to be_truthy + expect(subject.get('rbeapi')).to eq(nil) + end + + it 'remove the privilege with a value' do + expect(subject.set_privilege('rbeapi', value: '13', enable: false)) + .to be_truthy + expect(subject.get('rbeapi')).to eq(nil) + end + + it 'defaults the privilege without a value' do + expect(subject.set_privilege('rbeapi', default: true)).to be_truthy + expect(subject.get('rbeapi')).to eq(nil) + end + + it 'defaults the privilege with a value' do + expect(subject.set_privilege('rbeapi', value: '3', default: true)) + .to be_truthy + expect(subject.get('rbeapi')).to eq(nil) + end + end + + describe '#set_role' do + before do + node.config(['no username rbeapi', 'username rbeapi nopassword']) + end + + it 'set the role' do + expect(subject.set_role('rbeapi', value: 'net-minion')).to be_truthy + expect(subject.get('rbeapi')[:role]).to eq('net-minion') + end + + it 'remove the role without a value' do + expect(subject.set_role('rbeapi', enable: false)).to be_truthy + expect(subject.get('rbeapi')[:role]).to eq(nil) + end + + it 'remove the role with a value' do + expect(subject.set_role('rbeapi', value: 'net', enable: false)) + .to be_truthy + expect(subject.get('rbeapi')[:role]).to eq(nil) + end + + it 'defaults the role without a value' do + expect(subject.set_role('rbeapi', default: true)).to be_truthy + expect(subject.get('rbeapi')[:role]).to eq(nil) + end + + it 'defaults the role with a value' do + expect(subject.set_role('rbeapi', value: 'net', default: true)) + .to be_truthy + expect(subject.get('rbeapi')[:role]).to eq(nil) + end + end + + describe '#set_sshkey' do + before do + node.config(['no username rbeapi', 'username rbeapi nopassword']) + end + + it 'set the sshkey' do + expect(subject.set_sshkey('rbeapi', value: sshkey)).to be_truthy + end + + it 'remove the sshkey with a value' do + expect(subject.set_sshkey('rbeapi', value: sshkey, enable: false)) + .to be_truthy + expect(subject.get('rbeapi')[:sshkey]).to eq(nil) + end + + it 'defaults the sshkey without a value' do + expect(subject.set_sshkey('rbeapi', default: true)).to be_truthy + expect(subject.get('rbeapi')[:sshkey]).to eq(nil) + end + end +end diff --git a/spec/system/rbeapi/api/varp_interfaces_spec.rb b/spec/system/rbeapi/api/varp_interfaces_spec.rb index 276dae9..c161543 100644 --- a/spec/system/rbeapi/api/varp_interfaces_spec.rb +++ b/spec/system/rbeapi/api/varp_interfaces_spec.rb @@ -91,4 +91,38 @@ expect { subject.set_addresses('Vlan100') }.to raise_error ArgumentError end end + + describe '#add_address' do + before do + node.config(['ip virtual-router mac-address aabb.ccdd.eeff', + 'no interface Vlan99', 'no interface Vlan100', + 'default interface Vlan100', 'interface Vlan100', + 'ip address 99.99.99.99/24', 'exit']) + end + + it 'adds a new address to the list of addresses' do + expect(subject.get('Vlan100')[:addresses]).not_to include('99.99.99.98') + expect(subject.add_address('Vlan100', '99.99.99.98')) + .to be_truthy + expect(subject.get('Vlan100')[:addresses]).to include('99.99.99.98') + end + end + + describe '#remove_address' do + before do + node.config(['ip virtual-router mac-address aabb.ccdd.eeff', + 'no interface Vlan99', 'no interface Vlan100', + 'default interface Vlan100', 'interface Vlan100', + 'ip address 99.99.99.99/24', 'exit']) + end + + it 'removes the address from the list of addresses' do + expect(subject.add_address('Vlan100', '99.99.99.98')) + .to be_truthy + expect(subject.get('Vlan100')[:addresses]).to include('99.99.99.98') + expect(subject.remove_address('Vlan100', '99.99.99.98')) + .to be_truthy + expect(subject.get('Vlan100')[:addresses]).to eq([]) + end + end end diff --git a/spec/system/rbeapi/api/vrrp_spec.rb b/spec/system/rbeapi/api/vrrp_spec.rb new file mode 100644 index 0000000..486982e --- /dev/null +++ b/spec/system/rbeapi/api/vrrp_spec.rb @@ -0,0 +1,707 @@ +require 'spec_helper' + +require 'rbeapi/client' +require 'rbeapi/api/vrrp' + +describe Rbeapi::Api::Vrrp do + subject { described_class.new(node) } + + let(:node) do + Rbeapi::Client.config.read(fixture_file('dut.conf')) + Rbeapi::Client.connect_to('dut') + end + + before :all do + @sec_ips = ['1.2.3.1', '1.2.3.2', '1.2.3.3', '1.2.3.4'] + @tracks = [{ name: 'Ethernet3', action: 'decrement', amount: 33 }, + { name: 'Ethernet2', action: 'decrement', amount: 22 }, + { name: 'Ethernet2', action: 'shutdown' }] + end + + describe '#get' do + before do + node.config(['no interface Vlan150', + 'no interface Vlan100', + 'interface Vlan100', + 'interface Vlan150', + 'ip address 40.10.5.8/24', + 'vrrp 30 priority 100', + 'vrrp 30 timers advertise 1', + 'vrrp 30 mac-address advertisement-interval 30', + 'no vrrp 30 preempt', + 'vrrp 30 preempt delay reload 0', + 'vrrp 30 delay reload 0', + 'vrrp 30 ip 40.10.5.31', + 'vrrp 30 description The description', + 'vrrp 30 shutdown', + 'vrrp 30 track Ethernet1 decrement 5', + 'vrrp 30 ip version 2', + 'vrrp 40 priority 200', + 'vrrp 40 timers advertise 1', + 'vrrp 40 mac-address advertisement-interval 30', + 'vrrp 40 preempt', + 'vrrp 40 preempt delay reload 0', + 'vrrp 40 delay reload 0', + 'vrrp 40 ip 40.10.5.32', + 'vrrp 40 track Ethernet3 decrement 33', + 'vrrp 40 track Ethernet2 decrement 22', + 'vrrp 40 track Ethernet2 shutdown', + 'vrrp 40 ip version 2']) + end + + let(:entity) do + { 30 => { primary_ip: '40.10.5.31', delay_reload: 0, + description: 'The description', enable: false, ip_version: 2, + mac_addr_adv_interval: 30, preempt: false, preempt_delay_min: 0, + preempt_delay_reload: 0, priority: 100, secondary_ip: [], + timers_advertise: 1, + track: [ + { name: 'Ethernet1', action: 'decrement', amount: 5 } + ] + }, + 40 => { primary_ip: '40.10.5.32', delay_reload: 0, description: nil, + enable: true, ip_version: 2, mac_addr_adv_interval: 30, + preempt: true, preempt_delay_min: 0, preempt_delay_reload: 0, + priority: 200, secondary_ip: [], timers_advertise: 1, + track: @tracks + } + } + end + + it 'returns the virtual router resource' do + expect(subject.get('Vlan150')).to eq(entity) + end + end + + describe '#getall' do + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Hash) + end + + it 'returns the virtual router collection' do + expect(subject.getall).to include('Vlan100') + expect(subject.getall).to include('Vlan150') + end + end + + describe '#create' do + before do + node.config(['no interface Vlan100']) + end + + it 'creates a new virtual router with enable true' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, enable: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'creates a new virtual router with enable false' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to include(9) + end + + it 'creates a new virtual router with primary ip' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '1.2.3.4')).to be_truthy + expect(subject.get('Vlan100')[9][:primary_ip]).to eq('1.2.3.4') + end + + it 'creates a new virtual router with priority' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '1.2.3.4', + priority: 100)).to be_truthy + expect(subject.get('Vlan100')[9][:priority]).to eq(100) + end + + it 'creates a new virtual router with description' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, description: 'Desc')).to be_truthy + expect(subject.get('Vlan100')[9][:description]).to eq('Desc') + end + + it 'creates a new virtual router with secondary ips' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', 9, + secondary_ip: ['100.99.98.71', + '100.99.98.70'])).to be_truthy + expect(subject.get('Vlan100')[9][:secondary_ip]).to eq(['100.99.98.70', + '100.99.98.71']) + end + + it 'creates a new virtual router with ip version 2' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', + 9, + primary_ip: '100.99.98.100', + ip_version: 2)).to be_truthy + expect(subject.get('Vlan100')[9][:ip_version]).to eq(2) + end + + it 'creates a new virtual router with timers advertise' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, timers_advertise: 77)).to be_truthy + expect(subject.get('Vlan100')[9][:timers_advertise]).to eq(77) + end + + it 'creates a new virtual router with mac addr adv interval' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, mac_addr_adv_interval: 77)) + .to be_truthy + expect(subject.get('Vlan100')[9][:mac_addr_adv_interval]).to eq(77) + end + + it 'creates a new virtual router with preemt true' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', 9, preempt: true)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(true) + end + + it 'creates a new virtual router with preemt false' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, preempt: false)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(false) + end + + it 'creates a new virtual router with preempt delay min' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, preempt_delay_min: 100)) + .to be_truthy + expect(subject.get('Vlan100')[9][:preempt_delay_min]).to eq(100) + end + + it 'creates a new virtual router with preempt delay reload' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, preempt_delay_reload: 100)) + .to be_truthy + expect(subject.get('Vlan100')[9][:preempt_delay_reload]).to eq(100) + end + + it 'creates a new virtual router with preempt delay reload' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, delay_reload: 100)).to be_truthy + expect(subject.get('Vlan100')[9][:delay_reload]).to eq(100) + end + + it 'creates a new virtual router with track values' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', + 9, + track: [{ name: 'Ethernet3', + action: 'decrement', + amount: 33 }, + { name: 'Ethernet2', + action: 'decrement', + amount: 22 }, + { name: 'Ethernet2', + action: 'shutdown' }])).to be_truthy + expect(subject.get('Vlan100')[9][:track]).to eq(@tracks) + end + + it 'creates a new virtual router resource with enable and primary ip' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, enable: true, primary_ip: '1.2.3.4')) + .to be_truthy + expect(subject.get('Vlan100')[9][:primary_ip]).to eq('1.2.3.4') + end + + it 'creates a new virtual router resource with enable and priority' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', 9, enable: true, priority: 100)) + .to be_truthy + expect(subject.get('Vlan100')[9][:priority]).to eq(100) + end + + it 'creates a new virtual router resource with enable and description' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, enable: true, description: 'Desc')) + .to be_truthy + expect(subject.get('Vlan100')[9][:description]).to eq('Desc') + end + + it 'creates a new virtual router resource with enable and secondary_ip' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', + 9, + enable: true, + secondary_ip: ['1.2.3.1', + '1.2.3.2', + '1.2.3.3', '1.2.3.4'])).to be_truthy + expect(subject.get('Vlan100')[9][:secondary_ip]).to eq(@sec_ips) + end + + it 'creates a new virtual router resource with all options set' do + expect(subject.get('Vlan100')).to eq(nil) + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.create('Vlan100', + 9, + enable: true, + priority: 100, + description: 'Desc', + secondary_ip: ['100.99.98.71', + '100.99.98.70'], + ip_version: 2, + timers_advertise: 77, + mac_addr_adv_interval: 77, + preempt: true, + preempt_delay_min: 100, + preempt_delay_reload: 100, + delay_reload: 100, + track: [{ name: 'Ethernet3', + action: 'decrement', + amount: 33 }, + { name: 'Ethernet2', + action: 'decrement', + amount: 22 }, + { name: 'Ethernet2', + action: 'shutdown' }])).to be_truthy + end + + it 'raises ArgumentError for create without options' do + expect { subject.create('Vlan100', 9) }.to \ + raise_error ArgumentError + end + end + + describe '#delete' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'deletes a virtual router resource' do + expect(subject.delete('Vlan100', 9)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#default' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'sets virtual router resource to default' do + expect(subject.default('Vlan100', 9)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_shutdown' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable Vlan100 vrid 9' do + expect(subject.set_shutdown('Vlan100', 9)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'disable Vlan100 vrid 9' do + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.set_shutdown('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')[9][:enable]).to eq(false) + end + + it 'defaults Vlan100 vrid 9' do + expect(subject.set_shutdown('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_shutdown('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_primary_ip' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set primary IP address' do + expect(subject.set_primary_ip('Vlan100', 9, + value: '1.2.3.4')).to be_truthy + expect(subject.get('Vlan100')[9][:primary_ip]).to eq('1.2.3.4') + end + + it 'disable primary IP address' do + expect(subject.set_primary_ip('Vlan100', 9, value: '1.2.3.4', + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults primary IP address' do + expect(subject.set_primary_ip('Vlan100', 9, value: '1.2.3.4', + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_primary_ip('Vlan100', 9, enable: false, + value: '1.2.3.4', + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_priority' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set priority' do + expect(subject.set_priority('Vlan100', 9, value: 13)).to be_truthy + expect(subject.get('Vlan100')[9][:priority]).to eq(13) + end + + it 'disable priority' do + expect(subject.set_priority('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults priority' do + expect(subject.set_priority('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_priority('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_description' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set description' do + expect(subject.set_description('Vlan100', 9, + value: 'Howdy')).to be_truthy + expect(subject.get('Vlan100')[9][:description]).to eq('Howdy') + end + + it 'disable description' do + expect(subject.set_description('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults description' do + expect(subject.set_description('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_description('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_secondary_ip' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set secondary IP addresses' do + # Set current IP addresses + expect(subject.set_secondary_ip('Vlan100', 9, @sec_ips)).to be_truthy + expect(subject.get('Vlan100')[9][:secondary_ip]).to eq(@sec_ips) + end + + it 'remove all secondary IP addresses' do + # Set current IP addresses + expect(subject.set_secondary_ip('Vlan100', 9, @sec_ips)).to be_truthy + # Delete all IP addresses + expect(subject.set_secondary_ip('Vlan100', 9, [])).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_ip_version' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set VRRP version' do + expect(subject.set_ip_version('Vlan100', 9, value: 3)).to be_truthy + expect(subject.get('Vlan100')[9][:ip_version]).to eq(3) + end + + it 'disable VRRP version' do + expect(subject.set_ip_version('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults VRRP version' do + expect(subject.set_ip_version('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_ip_version('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_timers_advertise' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set advertise timer' do + expect(subject.set_timers_advertise('Vlan100', 9, value: 7)).to be_truthy + expect(subject.get('Vlan100')[9][:timers_advertise]).to eq(7) + end + + it 'disable advertise timer' do + expect(subject.set_timers_advertise('Vlan100', 9, + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults advertise timer' do + expect(subject.set_timers_advertise('Vlan100', 9, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_timers_advertise('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_mac_addr_adv_interval' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'set mac address advertisement interval' do + expect(subject.set_mac_addr_adv_interval('Vlan100', 9, + value: 12)).to be_truthy + expect(subject.get('Vlan100')[9][:mac_addr_adv_interval]).to eq(12) + end + + it 'disable mac address advertisement interval' do + expect(subject.set_mac_addr_adv_interval('Vlan100', 9, + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults mac address advertisement interval' do + expect(subject.set_mac_addr_adv_interval('Vlan100', 9, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_mac_addr_adv_interval('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_preempt' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable preempt mode' do + expect(subject.create('Vlan100', 9, primary_ip: '100.99.98.100')) + .to be_truthy + expect(subject.set_preempt('Vlan100', 9)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(true) + end + + it 'disable preempt mode' do + expect(subject.set_preempt('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(false) + end + + it 'defaults preempt mode' do + expect(subject.set_preempt('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(false) + end + + it 'default option takes precedence' do + expect(subject.set_preempt('Vlan100', 9, enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt]).to eq(false) + end + end + + describe '#set_preempt_delay_min' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable preempt mode' do + expect(subject.set_preempt_delay_min('Vlan100', 9, value: 8)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt_delay_min]).to eq(8) + end + + it 'disable preempt mode' do + expect(subject.set_preempt_delay_min('Vlan100', 9, + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults preempt mode' do + expect(subject.set_preempt_delay_min('Vlan100', 9, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_preempt_delay_min('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_preempt_delay_reload' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable preempt delay reload' do + expect(subject.set_preempt_delay_reload('Vlan100', 9, + value: 8)).to be_truthy + expect(subject.get('Vlan100')[9][:preempt_delay_reload]).to eq(8) + end + + it 'disable preempt delay reload' do + expect(subject.set_preempt_delay_reload('Vlan100', 9, + enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults preempt delay reload' do + expect(subject.set_preempt_delay_reload('Vlan100', 9, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_preempt_delay_reload('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_delay_reload' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + it 'enable delay reload' do + expect(subject.set_delay_reload('Vlan100', 9, value: 8)).to be_truthy + expect(subject.get('Vlan100')[9][:delay_reload]).to eq(8) + end + + it 'disable delay reload' do + expect(subject.set_delay_reload('Vlan100', 9, enable: false)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'defaults delay reload' do + expect(subject.set_delay_reload('Vlan100', 9, default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'default option takes precedence' do + expect(subject.set_delay_reload('Vlan100', 9, + enable: false, + default: true)).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + end + + describe '#set_tracks' do + before do + node.config(['no interface Vlan100', 'interface Vlan100', + 'vrrp 9 priority 100']) + end + + before :all do + @bad_key = [{ nombre: 'Ethernet3', action: 'decrement', amount: 33 }] + @miss_key = [{ action: 'decrement', amount: 33 }] + @bad_action = [{ name: 'Ethernet3', action: 'dec', amount: 33 }] + @sem_key = [{ name: 'Ethernet3', action: 'shutdown', amount: 33 }] + @bad_amount = [{ name: 'Ethernet3', action: 'decrement', amount: -1 }] + end + + it 'set tracks' do + # Set current IP addresses + expect(subject.set_tracks('Vlan100', 9, @tracks)).to be_truthy + expect(subject.get('Vlan100')[9][:track]).to eq(@tracks) + end + + it 'remove all tracks' do + # Set current IP addresses + expect(subject.set_tracks('Vlan100', 9, @tracks)).to be_truthy + # Delete all IP addresses + expect(subject.set_tracks('Vlan100', 9, [])).to be_truthy + expect(subject.get('Vlan100')).to eq({}) + end + + it 'raises ArgumentError for track hash with a bad key' do + expect { subject.set_tracks('Vlan100', 9, @bad_key) }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for track hash with missing required key' do + expect { subject.set_tracks('Vlan100', 9, @miss_key) }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for track hash with invalid action' do + expect { subject.set_tracks('Vlan100', 9, @bad_action) }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for track hash with shutdown and amount' do + expect { subject.set_tracks('Vlan100', 9, @sem_key) }.to \ + raise_error ArgumentError + end + + it 'raises ArgumentError for track hash with negative amount' do + expect { subject.set_tracks('Vlan100', 9, @bad_amount) }.to \ + raise_error ArgumentError + end + end +end diff --git a/spec/system/rbeapi/client_spec.rb b/spec/system/rbeapi/client_spec.rb new file mode 100644 index 0000000..a05dd99 --- /dev/null +++ b/spec/system/rbeapi/client_spec.rb @@ -0,0 +1,367 @@ +# +# 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. +# +require 'spec_helper' + +require 'rbeapi/client' + +describe Rbeapi::Client do + subject { described_class } + + def dut_conf + fixture_file('dut.conf') + end + + def test_conf + fixture_file('test.conf') + end + + let(:node) do + subject.config.read(fixture_file('dut.conf')) + subject.connect_to('dut') + end + + let(:dut) do + File.read(dut_conf) + end + + let(:test) do + File.read(test_conf) + end + + let(:enablepwd) { 'enable_admin' } + + let(:veos01) do + { + 'username' => 'eapi', + 'password' => 'password', + 'transport' => 'http', + 'host' => 'veos01' + } + end + + let(:veos05) do + { + 'host' => '172.16.131.40', + 'username' => 'admin', + 'password' => 'admin', + 'enablepwd' => 'password', + 'transport' => 'https', + 'port' => 1234, + 'open_timeout' => 12, + 'read_timeout' => 12 + } + end + + let(:test_data) do + [ + '[connection:veos01]' + ] + end + + # Client class methods + describe '#config_for' do + it 'returns the configuration options for the connection' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + + it 'returns nil if connection does not exist' do + expect(subject.config_for('veos22')).to eq(nil) + end + end + + describe '#connect_to' do + it 'retrieves the node config' do + expect(subject.connect_to('veos01')).to be_truthy + end + + it 'returns nil if connection does not exist' do + expect(subject.connect_to('veos22')).to eq(nil) + end + end + + describe '#load_config' do + it 'overrides the default conf file loaded in the config' do + expect(subject.load_config(test_conf)).to eq(nil) + end + + it 'returns nil if connection does not exit' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('dut')).to eq(nil) + end + + it 'returns conf settings if connection exists' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + # Config class methods + describe 'config' do + it 'gets the loaded configuration file data' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config.to_s).to include(test_data[0]) + end + end + + describe '#read' do + it 'read the specified filename and load dut' do + expect(subject.config.read(dut_conf)).to eq(nil) + expect(subject.config.to_s) + .to include('host', 'username', 'password', '[connection:dut]') + end + + it 'read the specified filename and load test' do + expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config.to_s).to include(test_data[0]) + end + end + + describe '#get_connection' do + it 'get connection veos01' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + end + + it 'get connection veos05' do + expect(subject.config.get_connection('veos05')).to eq(veos05) + end + end + + describe '#reload' do + it 'reloads the configuration file' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + expect(subject.config.reload(filename: [dut_conf])).to eq(nil) + expect(subject.config.get_connection('veos01')).to eq(nil) + expect(subject.config.get_connection('dut')).not_to be_nil + end + end + + describe '#add_connection' do + it 'adds a new connection section' do + expect(subject.config.add_connection('test2', + username: 'test2', + password: 'test', + transport: 'http', + host: 'test2' + )).to eq(nil) + expect(subject.config.get_connection('test2')) + .to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + end + end + + # Node Class Methods + describe 'node' do + it 'retrieves the node' do + expect(node).to be_kind_of(Rbeapi::Client::Node) + end + end + + describe '#running_config' do + it 'gets the nodes running config' do + expect(node.running_config).to be_truthy + end + + it 'expects running config to return a string' do + expect(node.running_config).to be_kind_of(String) + end + end + + describe '#startup_config' do + it 'gets the nodes startup-configuration' do + expect(node.startup_config).to be_truthy + end + + it 'expects startup-configuration to be a string' do + expect(node.startup_config).to be_kind_of(String) + end + end + + describe '#config' do + it 'puts switch into config mode' do + expect(node.config(['no ip virtual-router mac-address'])) + .to be_truthy + end + + it 'expects config to return array' do + expect(node.config(['no ip virtual-router mac-address'])) + .to be_kind_of(Array) + end + + it 'puts switch into config mode with options and returns array' do + expect(node.config(['no ip virtual-router mac-address'], + encoding: 'json', + open_timeout: 27.00, + read_timeout: 27.00)) + .to be_kind_of(Array) + end + + describe 'set dry run' do + before do + # Prevents puts from writing to console + allow($stdout).to receive(:puts) + node.dry_run = true + end + + it 'expects config to do dry run' do + expect(node.config(['no ip virtual-router mac-address'])) + .to eq(nil) + end + end + + it 'returns error if invalid command' do + expect { node.config(['no ip virtual-router mac-addresses']) } + .to raise_error Rbeapi::Eapilib::CommandError + end + end + + describe '#enable' do + it 'puts the switch into privilege mode' do + expect(node.enable('show hostname')[0][:result]) + .to include('fqdn', 'hostname') + end + + it 'puts the switch into privilege mode with encoding' do + expect(node.enable('show hostname', encoding: 'text')[0][:encoding]) + .to eq('text') + end + + it 'puts the switch into privilege mode with strict option' do + expect(node.enable('show hostname', strict: true)[0]) + .to include(:command, :result, :encoding) + end + + it 'puts the switch into privilege mode with read and open timeout' do + expect(node.enable('show hostname', + open_timeout: 29, + read_timeout: 29)[0]).to include(:command, + :result, + :encoding) + end + + it 'raises invalid command error' do + expect { node.enable(['show hostname', 'do this thing']) } + .to raise_error Rbeapi::Eapilib::CommandError + end + end + + describe '#run_commands' do + it 'expects run_commands to be a string' do + expect(node.run_commands('show hostname', encoding: 'text')[0]['output']) + .to be_kind_of String + end + + it 'sends commands to node with encoding' do + expect(node.run_commands('show hostname', encoding: 'text')[0]['output']) + .to include('FQDN:', 'Hostname:') + end + + it 'sends commands with open and read timeout' do + expect(node.run_commands('show hostname', + open_timeout: 26, + read_timeout: 26)[0]).to include('fqdn', + 'hostname') + end + + it 'expects run_commands to raise a command error' do + expect { node.run_commands('do this thing') } + .to raise_error Rbeapi::Eapilib::CommandError + end + end + + describe '#run_commands with enable password' do + # Before the tests Set the enable password on the dut + before(:each) { node.config(["enable secret 0 #{enablepwd}"]) } + + # After the tests clear the enable password on the dut + after(:each) { node.config(['no enable secret']) } + + it 'sends commands with enablepwd set' do + expect(node.enable_authentication(enablepwd)).to eq(enablepwd) + expect(node.run_commands('show hostname')).to be_truthy + end + end + + describe '#get_config' do + it 'will retrieve the specified configuration and return array' do + expect(node.get_config(config: 'running-config')) + .to be_kind_of(Array) + end + + it 'will retrieve with param and return array' do + expect(node.get_config(config: 'running-config', param: 'all')) + .to be_kind_of(Array) + end + + it 'raises invalid command error' do + expect { node.get_config(config: 'running-configurations') } + .to raise_error Rbeapi::Eapilib::CommandError + end + end + + describe '#api' do + it 'returns api module' do + expect(node.api('vlans')).to be_kind_of(Rbeapi::Api::Vlans) + end + + it 'returns error if invalid name' do + expect { node.api('vlanss') }.to raise_error + end + end + + describe '#refresh' do + it 'refreshes configs for next call' do + expect(node.refresh).to eq(nil) + end + end + + describe 'test timeouts' do + it 'loads default timeout values' do + expect(node.connection.get_timeouts).to eq(open_timeout: 10, + read_timeout: 10) + end + + describe 'loads veos05' do + let(:node) do + subject.config.read(fixture_file('test.conf')) + subject.connect_to('veos05') + end + + it 'loads timeout values from conf file' do + expect(node.connection.get_timeouts).to eq(open_timeout: 12, + read_timeout: 12) + end + end + end +end diff --git a/spec/unit/rbeapi/api/aaa/aaa_groups_spec.rb b/spec/unit/rbeapi/api/aaa/aaa_groups_spec.rb new file mode 100644 index 0000000..ba41189 --- /dev/null +++ b/spec/unit/rbeapi/api/aaa/aaa_groups_spec.rb @@ -0,0 +1,111 @@ +# +# 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. +# +require 'spec_helper' + +require 'rbeapi/api/aaa' + +include FixtureHelpers + +describe Rbeapi::Api::AaaGroups do + subject { described_class.new(node) } + + let(:node) { double('node') } + + let(:all) do + { + 'blah' => { + type: 'radius', + servers: [] + } + } + end + + let(:blah) do + { + type: 'radius', + servers: [] + } + end + + let(:blahthree) do + { + type: 'tacacs+', + servers: [] + } + end + + def aaa + aaa = Fixtures[:aaa] + return aaa if aaa + fixture('aaa', format: :text, dir: File.dirname(__FILE__)) + end + + before :each do + allow(subject.node).to receive(:running_config).and_return(aaa) + end + + describe '#get' do + it 'returns the resource for given name' do + expect(subject.get('blah')).to eq(blah) + end + end + + describe '#getall' do + it 'returns all of the aaa group resources' do + expect(subject.getall).to eq(all) + end + end + + describe '#create' do + it 'adds a new aaa group' do + expect(node).to receive(:config) + .with(['aaa group server tacacs+ blahthree', 'exit']) + expect(subject.create('blahthree', 'tacacs+')).to eq(true) + end + end + + describe '#delete' do + it 'removes specified aaa group' do + expect(subject.delete('blahthree')).to eq(true) + expect(subject.get('blahthree')).to eq(nil) + end + end + + describe '#set_servers' do + it 'removes all servers and then adds one' do + expect(node).to receive(:config) + .with(['aaa group server radius blah', 'server localhost ', 'exit']) + expect(subject.set_servers('blah', [{ name: 'localhost' }])) + .to eq(true) + end + end +end diff --git a/spec/unit/rbeapi/api/aaa/aaa_spec.rb b/spec/unit/rbeapi/api/aaa/aaa_spec.rb new file mode 100644 index 0000000..090bc71 --- /dev/null +++ b/spec/unit/rbeapi/api/aaa/aaa_spec.rb @@ -0,0 +1,77 @@ +# +# 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. +# +require 'spec_helper' + +require 'rbeapi/api/aaa' + +include FixtureHelpers + +describe Rbeapi::Api::Aaa do + subject { described_class.new(node) } + + let(:node) { double('node') } + + let(:test) do + { + groups: { + 'blah' => { + type: 'radius', + servers: [] + } + } + } + end + + def aaa + aaa = Fixtures[:aaa] + return aaa if aaa + fixture('aaa', format: :text, dir: File.dirname(__FILE__)) + end + + before :each do + allow(subject.node).to receive(:running_config).and_return(aaa) + end + + describe '#get' do + it 'returns the resource for given name' do + expect(subject.get).to eq(test) + end + + it 'returns a hash' do + expect(subject.get).to be_a_kind_of(Hash) + end + + it 'has two entries' do + expect(subject.get.size).to eq(1) + end + end +end diff --git a/spec/unit/rbeapi/api/aaa/fixture_aaa.text b/spec/unit/rbeapi/api/aaa/fixture_aaa.text new file mode 100644 index 0000000..78b8bd2 --- /dev/null +++ b/spec/unit/rbeapi/api/aaa/fixture_aaa.text @@ -0,0 +1,3 @@ +aaa group server radius blah +no aaa root +aaa authentication policy local allow-nopassword-remote-login \ No newline at end of file diff --git a/spec/unit/rbeapi/api/switchports/default_spec.rb b/spec/unit/rbeapi/api/switchports/default_spec.rb new file mode 100644 index 0000000..6217276 --- /dev/null +++ b/spec/unit/rbeapi/api/switchports/default_spec.rb @@ -0,0 +1,249 @@ +# +# Copyright (c) 2016, Arista Networks, Inc. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# Neither the name of Arista Networks nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ARISTA NETWORKS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN +# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +require 'spec_helper' + +require 'rbeapi/api/switchports' + +include FixtureHelpers + +describe Rbeapi::Api::Switchports do + subject { described_class.new(node) } + + let(:node) { double('node') } + + def switchports + switchports = Fixtures[:switchports] + return switchports if switchports + fixture('switchports', format: :text, dir: File.dirname(__FILE__)) + end + + before :each do + allow(subject.node).to receive(:running_config).and_return(switchports) + end + + describe '#get' do + let(:keys) do + [:mode, :access_vlan, :trunk_native_vlan, :trunk_allowed_vlans, + :trunk_groups] + end + + context 'vlan as an integer range' do + it 'returns the switchport resource' do + expect(subject.get('Ethernet1')).not_to be_nil + end + + it 'does not return a nonswitchport resource' do + expect(subject.get('Ethernet2')).to be_nil + end + + it 'has all required keys' do + expect(subject.get('Ethernet1').keys).to eq(keys) + end + + it 'returns allowed_vlans as an array' do + expect(subject.get('Ethernet1')[:trunk_allowed_vlans]) + .to be_a_kind_of(Array) + end + end + + context 'vlan as an integer' do + it 'returns the switchport resource' do + expect(subject.get('Ethernet1')).not_to be_nil + end + end + end + + describe '#getall' do + it 'returns the switchport collection' do + expect(subject.getall).to include('Ethernet1') + end + + it 'returns a hash collection' do + expect(subject.getall).to be_a_kind_of(Hash) + end + + it 'returns a hash collection' do + expect(subject.getall.count).to eq(1) + end + end + + describe '#create' do + it 'creates a new switchport resource' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no ip address', 'switchport']) + expect(subject.create('Ethernet1')).to be_truthy + end + end + + describe '#delete' do + it 'deletes a switchport resource' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport']) + expect(subject.delete('Ethernet1')).to be_truthy + end + end + + describe '#default' do + it 'sets Ethernet1 to default' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport']) + expect(subject.default('Ethernet1')).to be_truthy + end + end + + describe '#set_mode' do + it 'sets mode value to access' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport mode access']) + expect(subject.set_mode('Ethernet1', value: 'access')).to be_truthy + end + + it 'sets the mode value to trunk' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport mode trunk']) + expect(subject.set_mode('Ethernet1', value: 'trunk')).to be_truthy + end + + it 'negate the mode value' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport mode']) + expect(subject.set_mode('Ethernet1', enable: false)).to be_truthy + end + + it 'default the mode value' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport mode']) + expect(subject.set_mode('Ethernet1', default: true)).to be_truthy + end + end + + describe '#set_access_vlan' do + it 'sets the access vlan value to 100' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport access vlan 100']) + expect(subject.set_access_vlan('Ethernet1', value: '100')).to be_truthy + end + + it 'negates the access vlan value' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport access vlan']) + expect(subject.set_access_vlan('Ethernet1', enable: false)).to be_truthy + end + + it 'defaults the access vlan value' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport access vlan']) + expect(subject.set_access_vlan('Ethernet1', default: true)).to be_truthy + end + end + + describe '#set_trunk_native_vlan' do + it 'sets the trunk native vlan to 100' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport trunk native vlan 100']) + expect(subject.set_trunk_native_vlan('Ethernet1', value: '100')) + .to be_truthy + end + + it 'negates the trunk native vlan' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport trunk native vlan']) + expect(subject.set_trunk_native_vlan('Ethernet1', enable: false)) + .to be_truthy + end + + it 'defaults the trunk native vlan' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport trunk native vlan']) + expect(subject.set_trunk_native_vlan('Ethernet1', default: true)) + .to be_truthy + end + end + + describe '#set_trunk_allowed_vlans' do + it 'raises an ArgumentError if value is not an array' do + expect { subject.set_trunk_allowed_vlans('Ethernet1', value: '1-100') } + .to raise_error(ArgumentError) + end + + it 'sets vlan 8 and 9 to the trunk allowed vlans' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport trunk allowed vlan none', + 'switchport trunk allowed vlan 8,9']) + expect(subject.set_trunk_allowed_vlans('Ethernet1', value: [8, 9])) + .to be_truthy + end + + it 'negate switchport trunk allowed vlan' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport trunk allowed vlan']) + expect(subject.set_trunk_allowed_vlans('Ethernet1', enable: false)) + .to be_truthy + end + + it 'default switchport trunk allowed vlan' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport trunk allowed vlan']) + expect(subject.set_trunk_allowed_vlans('Ethernet1', default: true)) + .to be_truthy + end + end + + describe '#set_trunk_groups' do + it 'raises an ArgumentError if value is not an array' do + expect { subject.set_trunk_groups('Ethernet1', value: 'foo') } + .to raise_error(ArgumentError) + end + + it 'sets trunk group to foo bar bang' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'switchport trunk group foo', + 'switchport trunk group bar', 'switchport trunk group bang']) + expect(subject.set_trunk_groups('Ethernet1', value: %w(foo bar bang))) + .to be_truthy + end + + it 'negate switchport trunk group' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'no switchport trunk group']) + expect(subject.set_trunk_groups('Ethernet1', enable: false)) + .to be_truthy + end + + it 'default switchport trunk group' do + expect(node).to receive(:config) + .with(['interface Ethernet1', 'default switchport trunk group']) + expect(subject.set_trunk_groups('Ethernet1', default: true)) + .to be_truthy + end + end +end diff --git a/spec/unit/rbeapi/api/switchports/fixture_switchports.text b/spec/unit/rbeapi/api/switchports/fixture_switchports.text new file mode 100644 index 0000000..060b80c --- /dev/null +++ b/spec/unit/rbeapi/api/switchports/fixture_switchports.text @@ -0,0 +1,284 @@ +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 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 +! +interface Ethernet2 + no description + no shutdown + default load-interval + mtu 1500 + logging event link-status use-global + no dcbx mode + no mac-address + no link-debounce + no flowcontrol send + no flowcontrol receive + no mac timestamp + no speed + no l2 mtu + default logging event congestion-drops + default unidirectional + no 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 + no ip address + 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 Ethernet2 + 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 + no channel-group + lacp rate normal + lacp port-priority 32768 + lldp transmit + lldp receive + ip mfib fastdrop + no msrp + no mvrp + 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 + 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 +! diff --git a/spec/unit/rbeapi/api/users/default_spec.rb b/spec/unit/rbeapi/api/users/default_spec.rb index c395f67..af04395 100644 --- a/spec/unit/rbeapi/api/users/default_spec.rb +++ b/spec/unit/rbeapi/api/users/default_spec.rb @@ -96,7 +96,7 @@ def users expect(subject.getall).to be_a_kind_of(Hash) end - it 'has two entries' do + it 'has three entries' do expect(subject.getall.size).to eq(3) end end diff --git a/spec/unit/rbeapi/client_spec.rb b/spec/unit/rbeapi/client_spec.rb new file mode 100644 index 0000000..4af7038 --- /dev/null +++ b/spec/unit/rbeapi/client_spec.rb @@ -0,0 +1,211 @@ +# +# 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. +# +require 'spec_helper' + +require 'rbeapi/client' + +include FixtureHelpers + +describe Rbeapi::Client do + subject { described_class } + + def dut_conf + fixture_file('dut.conf') + end + + def test_conf + fixture_file('test.conf') + end + + def empty_conf + fixture_file('empty.conf') + end + + def yaml_conf + fixture_file('eapi.conf.yaml') + end + + def wildcard_conf + fixture_file('wildcard.conf') + end + + let(:dut) do + File.read(dut_conf) + end + + let(:test) do + File.read(test_conf) + end + + let(:veos01) do + { + 'username' => 'eapi', + 'password' => 'password', + 'transport' => 'http', + 'host' => 'veos01' + } + end + + let(:veos05) do + { + 'host' => '172.16.131.40', + 'username' => 'admin', + 'password' => 'admin', + 'enablepwd' => 'password', + 'transport' => 'https', + 'port' => 1234, + 'open_timeout' => 12, + 'read_timeout' => 12 + } + end + + let(:wildcard) do + { + 'username' => 'foo', + 'password' => 'bar', + 'host' => '*' + } + end + + let(:test_data) do + [ + '[connection:veos01]' + ] + end + + let(:default_entry) { "[connection:localhost]\ntransport : socket\n" } + + # Client class methods + describe '#config_for' do + # Verify that the EAPI_CONF env variable path is used by default + # when the Config class is instantiated/reload-ed. + it 'env path to config file' do + # Store env path for the eapi conf file and reload the class + conf = fixture_dir + '/env_path.conf' + ENV.store('EAPI_CONF', conf) + subject.config.reload + + # Verify env_path.conf file was loaded + expect(subject.config.to_s).to include('[connection:env_path]') + end + + it 'returns the configuration options for the connection' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + describe '#connect_to' do + it 'retrieves the node config' do + expect(subject.connect_to('veos01')).to be_truthy + end + + it 'check connection wildcard host name' do + expect(subject.load_config(wildcard_conf)).to eq(nil) + expect(subject.connect_to('host1')).to be_truthy + expect(subject.config.get_connection('host1')) .to eq(wildcard) + expect(subject.connect_to('host2')).to be_truthy + expect(subject.config.get_connection('host2')) .to eq(wildcard) + end + end + + describe '#load_config' do + it 'overrides the default conf file loaded in the config' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config_for('dut')).to eq(nil) + expect(subject.config_for('veos01')).to eq(veos01) + end + end + + # Config class methods + describe 'config' do + it 'gets the loaded configuration file data' do + expect(subject.load_config(test_conf)).to eq(nil) + expect(subject.config.to_s).to include(test_data[0]) + end + + it 'loading empty config file does not fail' do + expect(subject.load_config(empty_conf)).to eq(nil) + expect(subject.config.to_s).to eq(default_entry) + end + + it 'does not load bad config file data' do + expect(subject.load_config(yaml_conf)).to eq(nil) + expect(subject.config.to_s).to eq('') + end + end + + describe '#read' do + it 'read the specified filename and load it' do + expect(subject.load_config(dut_conf)).to eq(nil) + expect(subject.config.read(test_conf)).to eq(nil) + expect(subject.config.to_s).to include(test_data[0]) + end + end + + describe '#get_connection' do + it 'get connection dut' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + end + + it 'connection wildcard works' do + expect(subject.load_config(wildcard_conf)).to eq(nil) + expect(subject.config.get_connection('host1')) .to eq(wildcard) + expect(subject.config.get_connection('host2')) .to eq(wildcard) + end + end + + describe '#reload' do + it 'reloads the configuration file' do + expect(subject.config.get_connection('veos01')).to eq(veos01) + expect(subject.config.reload(filename: [dut_conf])).to eq(nil) + expect(subject.config.get_connection('veos01')).to eq(nil) + expect(subject.config.get_connection('dut')).not_to be_nil + end + end + + describe '#add_connection' do + it 'adds a new connection section' do + expect(subject.config.add_connection('test2', + username: 'test2', + password: 'test', + transport: 'http', + host: 'test2' + )).to eq(nil) + expect(subject.config.get_connection('test2')) + .to eq(username: 'test2', + password: 'test', + transport: 'http', + host: 'test2') + end + end +end