Skip to content

Commit

Permalink
Merge pull request #127 from CiscoTestAutomation/ise_connector
Browse files Browse the repository at this point in the history
Base ISE connector implementation, refactor abstraction
  • Loading branch information
dwapstra authored Nov 5, 2024
2 parents 75cc3d0 + c8e1048 commit b289e2d
Show file tree
Hide file tree
Showing 24 changed files with 231 additions and 23 deletions.
10 changes: 4 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Author:
#
# Support:
# pyats-support@cisco.com
# pyats-support-ext@cisco.com
#
# Version:
# v1.0.0
Expand All @@ -23,14 +23,12 @@
# Variables
PKG_NAME = rest.connector
BUILDDIR = $(shell pwd)/__build__
PROD_USER = pyadm@pyats-ci
PROD_PKGS = /auto/pyats/packages
STAGING_PKGS = /auto/pyats/staging/packages
STAGING_EXT_PKGS = /auto/pyats/staging/packages_external
PYTHON = python3
TESTCMD = $(PYTHON) -m unittest discover tests
DISTDIR = $(BUILDDIR)/dist

DEPENDENCIES = f5-icontrol-rest requests_mock requests dict2xml ciscoisesdk

.PHONY: clean package distribute distribute_staging distribute_staging_external\
develop undevelop populate_dist_dir help docs pubdocs tests

Expand Down Expand Up @@ -93,7 +91,7 @@ develop:
@echo ""

@pip uninstall -y rest.connector || true
@pip install f5-icontrol-rest requests_mock requests dict2xml
@pip install $(DEPENDENCIES)
@$(PYTHON) setup.py develop --no-deps -q

@echo ""
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog/undistributed/changelog_ise_20240711163922.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
--------------------------------------------------------------------------------
New
--------------------------------------------------------------------------------
* connector
* Add ISE connector base implementation
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ def find_version(*paths):
'requests >= 1.15.1',
'dict2xml',
'f5-icontrol-rest',
'ciscoisesdk'
],

# any additional groups of dependencies.
Expand Down
2 changes: 1 addition & 1 deletion src/rest/connector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def __getattribute__(self, name):
# Selector of methods/attributes to pick from abstracted
# Can't use __getattr__ as BaseConnection is abstract and some already
# exists
if name in ['get', 'post', 'put', 'patch', 'delete',
if name in ['api', 'get', 'post', 'put', 'patch', 'delete',
'connect', 'disconnect', 'connected']:
return getattr(self._implementation, name)

Expand Down
2 changes: 1 addition & 1 deletion src/rest/connector/libs/apic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='apic')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/bigip/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='bigip')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/dcnm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='dcnm')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/dnac/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='dnac')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/elasticsearch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='elasticsearch')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/iosxe/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='iosxe')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 2 additions & 0 deletions src/rest/connector/libs/ise/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from genie import abstract
abstract.declare_token(os='ise')
144 changes: 144 additions & 0 deletions src/rest/connector/libs/ise/implementation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@

import logging
import urllib.request
from requests.exceptions import RequestException

from pyats.connections import BaseConnection
from rest.connector.utils import get_username_password
from rest.connector.implementation import Implementation as RestImplementation

from ciscoisesdk import IdentityServicesEngineAPI

# create a logger for this module
log = logging.getLogger(__name__)


class Implementation(RestImplementation):
'''Rest Implementation for ISE
Implementation of Rest connection to ISE servers
YAML Example
------------
devices:
ise:
os: ise
connections:
rest:
class: rest.connector.Rest
ip: 127.0.0.1
port: "443"
protocol: https
credentials:
rest:
username: admin
password: admin
Code Example
------------
>>> from pyats.topology import loader
>>> testbed = loader.load('topology.yaml')
>>> device = testbed.devices['ise']
>>> device.connect(alias='rest', via='rest')
>>> device.rest.connected
True
'''

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'proxies' not in kwargs:
self.proxies = urllib.request.getproxies()

@BaseConnection.locked
def connect(self,
timeout=30,
verbose=False,
port="443",
protocol='https',
debug=False,
**kwargs):
'''connect to the device via REST
Arguments
---------
timeout (int): Timeout value
Raises
------
Exception
---------
If the connection did not go well
'''
if self.connected:
log.info(f'{self} already connected')
return

# support sshtunnel
if 'sshtunnel' in self.connection_info:
try:
from unicon.sshutils import sshtunnel
except ImportError:
raise ImportError(
'`unicon` is not installed for `sshtunnel`. Please install by `pip install unicon`.'
)
try:
tunnel_port = sshtunnel.auto_tunnel_add(self.device, self.via)
if tunnel_port:
ip = self.device.connections[self.via].sshtunnel.tunnel_ip
port = tunnel_port
except AttributeError as e:
raise AttributeError(
"Cannot add ssh tunnel. Connection %s may not have ip/host or port.\n%s"
% (self.via, e))
else:
ip = self.connection_info.ip.exploded
port = self.connection_info.get('port', port)

if 'protocol' in self.connection_info:
protocol = self.connection_info['protocol']

self.base_url = '{protocol}://{ip}:{port}'.format(protocol=protocol,
ip=ip,
port=port)

username, password = get_username_password(self)

self.api = IdentityServicesEngineAPI(
username=username, password=password,
base_url=self.base_url, uses_api_gateway=True,
verify=False, debug=debug)

version_request = self.api.version_and_patch.get_ise_version_and_patch()
if version_request.status_code == 200:
version_info = version_request.response
# {'OperationResult': {'resultValue': [
# {'value': '3.3.0.430', 'name': 'version'}, {'value': '0', 'name': 'patch information'}]}}
version_info_list = version_info.get('OperationResult', {}).get('resultValue', [])
version_message_list = []
for version_nv in version_info_list:
name = version_nv.get('name')
value = version_nv.get('value')
if name and value:
version_message_list.append(f'{name}: {value}')
if version_message_list:
version_message = ' '.join(version_message_list)
log.info(version_message)
self._is_connected = True
log.info("Connected successfully to '{d}'".format(d=self.device.name))
else:
raise ConnectionError('Unable to connect to ISE server')

return self.api

@BaseConnection.locked
def disconnect(self):
"""
"""
self._is_connected = False
return
2 changes: 1 addition & 1 deletion src/rest/connector/libs/nd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='nd')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/nexusdashboard/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='nexusdashboard')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/nso/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='nso')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/nxos/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='nxos')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/nxos/aci/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(platform='aci')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/viptela/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='viptela')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/virl/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='virl')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/vmware/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='vmware')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/webex/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='webex')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
2 changes: 1 addition & 1 deletion src/rest/connector/libs/xpresso/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Enable abstraction using this directory name as the abstraction token
try:
from genie import abstract
abstract.declare_token(__name__)
abstract.declare_token(os='xpresso')
except Exception as e:
import warnings
warnings.warn('Could not declare abstraction token: ' + str(e))
48 changes: 48 additions & 0 deletions src/rest/connector/tests/test_ise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/bin/env python
""" Unit tests for the rest.connector cisco-shared package. """
import os
import unittest
from requests.models import Response
from unittest.mock import patch, MagicMock

from pyats.topology import loader

from rest.connector import Rest
HERE = os.path.dirname(__file__)


class test_rest_connector(unittest.TestCase):
def setUp(self):
self.testbed = loader.load(os.path.join(HERE, 'testbed.yaml'))
self.device = self.testbed.devices['ise']

def test_init(self):
connection = Rest(device=self.device, alias='rest', via='rest')
self.assertEqual(connection.device, self.device)

with self.assertRaises(NotImplementedError):
self.assertRaises(connection.execute())
with self.assertRaises(NotImplementedError):
self.assertRaises(connection.configure())

def test_connection(self):
connection = Rest(device=self.device, alias='rest', via='rest')
self.assertEqual(connection.connected, False)

with patch('requests.session') as req:
resp = Response()
resp.headers['Content-type'] = 'application/json'
resp.status_code = 200
resp._content = b'{"OperationResult": {"resultValue": [{"value": "3.3.0.430",' + \
b' "name": "version"}, {"value": "0", "name": "patch information"}]}}'
req().request.return_value = resp
connection.connect()
self.assertEqual(connection.connected, True)
connection.connect()
self.assertEqual(connection.connected, True)

# Now disconnect
with patch('requests.session') as req:
connection.disconnect()
self.assertEqual(connection.connected, False)

Loading

0 comments on commit b289e2d

Please sign in to comment.