Skip to content

Commit

Permalink
Merge pull request #136 from arista-eosplus/release-0.8.0
Browse files Browse the repository at this point in the history
Release 0.8.0
  • Loading branch information
mharista authored Mar 14, 2017
2 parents 07dc94d + 768db47 commit b046cce
Show file tree
Hide file tree
Showing 48 changed files with 2,368 additions and 139 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ recursive-include test *.ospf
recursive-include test *.routemaps
recursive-include test *.varp
recursive-include test *.varp_null
recursive-include test *.vrf
recursive-include test *.vrrp
recursive-include test *.yaml
recursive-include docs description.rst
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Arista eAPI Python Library

[![Build Status](https://travis-ci.org/arista-eosplus/pyeapi.svg?branch=develop)](https://travis-ci.org/arista-eosplus/pyeapi) [![Coverage Status](https://coveralls.io/repos/arista-eosplus/pyeapi/badge.svg?branch=develop)](https://coveralls.io/r/arista-eosplus/pyeapi?branch=develop) [![Documentation Status](https://readthedocs.org/projects/pyeapi/badge/?version=latest)](http://readthedocs.org/docs/pyeapi/en/latest/?badge=latest)
[![Build Status](https://travis-ci.org/arista-eosplus/pyeapi.svg?branch=develop)](https://travis-ci.org/arista-eosplus/pyeapi) [![Coverage Status](https://coveralls.io/repos/github/arista-eosplus/pyeapi/badge.svg?branch=develop)](https://coveralls.io/github/arista-eosplus/pyeapi?branch=develop) [![Documentation Status](https://readthedocs.org/projects/pyeapi/badge/?version=latest)](http://readthedocs.org/docs/pyeapi/en/latest/?badge=latest)

The Python library for Arista's eAPI command API implementation provides a
client API work using eAPI and communicating with EOS nodes. The Python
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.0
0.8.0
8 changes: 8 additions & 0 deletions docs/examples.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Configuration Examples Using pyeapi
===================================


.. toctree::
:maxdepth: 1

subinterfaces
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ through Github issues.
configfile
modules
requirements
examples
contribute
release-notes
support
Expand Down
37 changes: 37 additions & 0 deletions docs/release-notes-0.8.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
######
v0.8.0
######

2017-03-14

New Modules
^^^^^^^^^^^

* Base API for VRF support. (`133 <https://github.com/arista-eosplus/pyeapi/pull/133>`_) [`mharista <https://github.com/mharista>`_]
Added new API module for creating VRFs. In addition to creating, configuring and removing VRFs the updates allow for applying a VRF to an interface and creating a VRF specific OSPF instance.

Enhancements
^^^^^^^^^^^^

* Add base extended ACL support. (`135 <https://github.com/arista-eosplus/pyeapi/pull/135>`_) [`mharista <https://github.com/mharista>`_]
Updated ACL api to include extended ACLs in addition to standard. To create an extended ACL provide the type as extended when creating the ACL (default is standard). Currently extended ACL statements can be added with action, protocol, and source/destination address. The API will determine the type of ACL by name after it has been created for future updates.
* Add support for creating and deleting ethernet subinterfaces (`132 <https://github.com/arista-eosplus/pyeapi/pull/132>`_) [`mharista <https://github.com/mharista>`_]
Allow for creation and deletion of ethernet subinterfaces as part of the EthernetInterface class. Subinterfaces are also supported on PortChannel interfaces. An example using the API to create an ethernet subinterface is provided in the docs.
* Add node attributes from show version command (`131 <https://github.com/arista-eosplus/pyeapi/pull/131>`_) [`mharista <https://github.com/mharista>`_]
Added information from show version as attributes to a node. Version, version number and model are added. Version number is simply the numeric portion of the version. For example 4.17.1 if the version is 4.17.1M. All three parameters are populated from the output of show version when any one of the parameters is accessed the first time.
* Add support for eAPI expandAliases parameter (`127 <https://github.com/arista-eosplus/pyeapi/pull/127>`_) [`mharista <https://github.com/mharista>`_]
Allowed users to provide the expandAliases parameter to eAPI calls. This allows users to use aliased commands via the API. For example if an alias is configured as 'sv' for 'show version' then an API call with sv and the expandAliases parameter will return the output of show version.
* Add support for autoComplete parameter. Issue 119 (`123 <https://github.com/arista-eosplus/pyeapi/pull/123>`_) [`mharista <https://github.com/mharista>`_]
Allows users to use short hand commands in eAPI calls. With this parameter included a user can send 'sh ver' via eAPI to get the output of show version.
* expose portchannel attributes :lacp fallback, lacp fallback timeout (`118 <https://github.com/arista-eosplus/pyeapi/pull/118>`_) [`lruslan <https://github.com/lruslan>`_]
Helps for configuring LACP fallback in EOS.

Fixed
^^^^^

* API path is hardcoded in EapiConnection.send() (`129 <https://github.com/arista-eosplus/pyeapi/issues/129>`_)
Updated the previously hardcoded path to /command-api in the EAPI connection to use the transport objects path.
* Cannot run 'no ip virtual-router mac-address' in eos 4.17.x (`122 <https://github.com/arista-eosplus/pyeapi/issues/122>`_)
Fixed command format for negating an ip virtual-router mac-address. Default and disable forms of the command changed and require the mac-address value in EOS 4.17. Update fixes this for newer versions and is backwards compatible.
* Non-standard HTTP/s port would cause connection to fail (`120 <https://github.com/arista-eosplus/pyeapi/issues/120>`_)
Bug fixed in PR (`121 <https://github.com/arista-eosplus/pyeapi/pull/121>`_) where a port passed in via eapi.conf as a unicode value caused the http connection to fail.
1 change: 1 addition & 0 deletions docs/release-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Release Notes
:maxdepth: 2
:titlesonly:

release-notes-0.8.0.rst
release-notes-0.7.0.rst
release-notes-0.6.1.rst
release-notes-0.6.0.rst
Expand Down
58 changes: 58 additions & 0 deletions docs/subinterfaces.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
Configuring Subinterfaces Using Python Client for eAPI
=======================================================

Subinterfaces can be configured on Ethernet and Port-Channel interfaces. To do this in
pyeapi simply call create or delete with your subinterface name.

Subinterfaces require that the primary interface be in routed mode.

.. code-block:: python
import pyeapi
node = pyeapi.connect_to('veos01')
node.api('ipinterfaces').create('Ethernet1')
At this point the below should be in your running configuration.

.. code-block:: shell
!
interface Ethernet1
no switchport
!
Next step is to create the subinterface

.. code-block:: python
node.api('interfaces').create('Ethernet1.1')
.. code-block:: shell
!
interface Ethernet1
no switchport
!
interface Ethernet1.1
!
Subinterfaces also require a vlan to be applied to them.

.. code-block:: python
node.api('interfaces').set_encapsulation('Ethernet1.1', 4)
.. code-block:: shell
!
interface Ethernet1
no switchport
!
interface Ethernet1.1
encapsulation dot1q vlan 4
!
Using delete in the same format as create will remove the subinterface.

For more detailed information about configuring subinterfaces in EOS, reference the user
manual for the version of EOS running on your switch.
2 changes: 1 addition & 1 deletion pyeapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
__version__ = '0.7.0'
__version__ = '0.8.0'
__author__ = 'Arista EOS+'


Expand Down
180 changes: 163 additions & 17 deletions pyeapi/api/acl.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@
import netaddr

from pyeapi.api import EntityCollection
from pyeapi.utils import ProxyCall

VALID_ACLS = frozenset(['standard', 'extended'])


def mask_to_prefixlen(mask):
Expand Down Expand Up @@ -76,6 +79,75 @@ def prefixlen_to_mask(prefixlen):
return str(netaddr.IPNetwork(addr).netmask)


class Acls(EntityCollection):

def __init__(self, node, *args, **kwargs):
super(Acls, self).__init__(node, *args, **kwargs)
self._instances = dict()

def get(self, name):
return self.get_instance(name)[name]

def getall(self):
"""Returns all ACLs in a dict object.
Returns:
A Python dictionary object containing all ACL
configuration indexed by ACL name::
{
"<ACL1 name>": {...},
"<ACL2 name>": {...}
}
"""
acl_re = re.compile(r'^ip access-list (?:(standard) )?(.+)$', re.M)
response = {'standard': {}, 'extended': {}}
for acl_type, name in acl_re.findall(self.config):
acl = self.get(name)
if acl_type and acl_type == 'standard':
response['standard'][name] = acl
else:
response['extended'][name] = acl
return response

def __getattr__(self, name):
return ProxyCall(self.marshall, name)

def marshall(self, name, *args, **kwargs):
acl_name = args[0]
acl_instance = self.get_instance(acl_name)
if not hasattr(acl_instance, name):
raise AttributeError("'%s' object has no attribute '%s'" %
(acl_instance, name))
method = getattr(acl_instance, name)
return method(*args, **kwargs)

def get_instance(self, name):
if name in self._instances:
return self._instances[name]
acl_re = re.compile(r'^ip access-list (?:(standard) )?(%s)$' % name,
re.M)
match = acl_re.search(self.config)
if match:
acl_type = match.group(1) or 'extended'
return self.create_instance(match.group(2), acl_type)
return {name: None}

def create_instance(self, name, acl_type):
if acl_type not in VALID_ACLS:
acl_type = 'standard'
acl_instance = ACL_CLASS_MAP.get(acl_type)
self._instances[name] = acl_instance(self.node)
return self._instances[name]

def create(self, name, type='standard'):
# Create ACL instance for ACL type Standard or Extended then call
# create method for specific ACL class.
acl_instance = self.create_instance(name, type)
return acl_instance.create(name)


class StandardAcls(EntityCollection):

entry_re = re.compile(r'(\d+)'
Expand All @@ -91,30 +163,21 @@ def get(self, name):
config = self.get_block('ip access-list standard %s' % name)
if not config:
return None

resource = dict(name=name, type='standard')
resource.update(self._parse_entries(config))
return resource

def getall(self):
resources = dict()
acls = re.compile(r'ip access-list standard ([^\s]+)')
for name in acls.findall(self.config):
resources[name] = self.get(name)
return resources

def _parse_entries(self, config):
entries = dict()
for item in re.finditer(r'\d+ [p|d].*$', config, re.M):
match = self.entry_re.match(item.group(0))
if match:
(seq, act, anyip, host, ip, mlen, mask, log) = match.groups()
entry = dict()
entry['action'] = act
entry['srcaddr'] = ip or '0.0.0.0'
entry['srclen'] = mlen or mask_to_prefixlen(mask)
entry['log'] = log is not None
entries[seq] = entry
(seq, act, anyip, host, ip, mlen, mask, log) = match.groups()
entry = dict()
entry['action'] = act
entry['srcaddr'] = ip or '0.0.0.0'
entry['srclen'] = mlen or mask_to_prefixlen(mask)
entry['log'] = log is not None
entries[seq] = entry
return dict(entries=entries)

def create(self, name):
Expand Down Expand Up @@ -152,5 +215,88 @@ def remove_entry(self, name, seqno):
return self.configure(cmds)


class ExtendedAcls(EntityCollection):

entry_re = re.compile(r'(\d+)'
r'(?: ([p|d]\w+))'
r'(?: (\w+|\d+))'
r'(?: ([a|h]\w+))?'
r'(?: ([0-9]+(?:\.[0-9]+){3}))?'
r'(?:/([0-9]{1,2}))?'
r'(?: ((?:eq|gt|lt|neq|range) [\w-]+))?'
r'(?: ([a|h]\w+))?'
r'(?: ([0-9]+(?:\.[0-9]+){3}))?'
r'(?:/([0-9]{1,2}))?'
r'(?: ([0-9]+(?:\.[0-9]+){3}))?'
r'(?: ((?:eq|gt|lt|neq|range) [\w-]+))?'
r'(?: (.+))?')

def get(self, name):
config = self.get_block('ip access-list %s' % name)
if not config:
return None
resource = dict(name=name, type='extended')
resource.update(self._parse_entries(config))
return resource

def _parse_entries(self, config):
entries = dict()
for item in re.finditer(r'\d+ [p|d].*$', config, re.M):
match = self.entry_re.match(item.group(0))
entry = dict()
entry['action'] = match.group(2)
entry['protocol'] = match.group(3)
entry['srcaddr'] = match.group(5) or 'any'
entry['srclen'] = match.group(6)
entry['srcport'] = match.group(7)
entry['dstaddr'] = match.group(9) or 'any'
entry['dstlen'] = match.group(10)
entry['dstport'] = match.group(12)
entry['other'] = match.group(13)
entries[match.group(1)] = entry
return dict(entries=entries)

def create(self, name):
return self.configure('ip access-list %s' % name)

def delete(self, name):
return self.configure('no ip access-list %s' % name)

def default(self, name):
return self.configure('default ip access-list %s' % name)

def update_entry(self, name, seqno, action, protocol, srcaddr,
srcprefixlen, dstaddr, dstprefixlen, log=False):
cmds = ['ip access-list %s' % name]
cmds.append('no %s' % seqno)
entry = '%s %s %s %s/%s %s/%s' % (seqno, action, protocol, srcaddr,
srcprefixlen, dstaddr, dstprefixlen)
if log:
entry += ' log'
cmds.append(entry)
cmds.append('exit')
return self.configure(cmds)

def add_entry(self, name, action, protocol, srcaddr, srcprefixlen,
dstaddr, dstprefixlen, log=False, seqno=None):
cmds = ['ip access-list %s' % name]
entry = '%s %s %s/%s %s/%s' % (action, protocol, srcaddr,
srcprefixlen, dstaddr, dstprefixlen)
if seqno is not None:
entry = '%s %s' % (seqno, entry)
if log:
entry += ' log'
cmds.append(entry)
cmds.append('exit')
return self.configure(cmds)

def remove_entry(self, name, seqno):
cmds = ['ip access-list %s' % name, 'no %s' % seqno, 'exit']
return self.configure(cmds)


ACL_CLASS_MAP = {'standard': StandardAcls, 'extended': ExtendedAcls}


def instance(node):
return StandardAcls(node)
return Acls(node)
Loading

0 comments on commit b046cce

Please sign in to comment.