Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Changelog
> * Pre-v1.00, 11.2020 -- Initial release - Marcel Zehnder
> * v1.00, 11.2020 -- Adjustments for OpenSource - Andreas Graber
> * v1.03, 06.2021 -- Snapshot creation added - Dario Kaelin
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ A python wrapper to the Cisco ACI REST-API.
We support Python 3.6 and up. Python 2 is not supported and there is no plan to add support for it.

## Installation
``pip install aciclient``
``pip install aciClient``

## Installation for Developing
```
Expand All @@ -28,7 +28,7 @@ import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

aciclient = aciClient.ACI(apic_hostname, apic_username, apic_password)
aciclient = aciClient.ACI(apic_hostname, apic_username, apic_password, refresh=False)
try:
aciclient.login()

Expand All @@ -41,6 +41,11 @@ except Exception as e:
logger.exception("Stack Trace")

```
For automatic authentication token refresh you can set refresh to True
```python
aciclient = aciClient.ACI(apic_hostname, apic_username, apic_password, refresh=True)
```


### Certificate/signature
```python
Expand Down Expand Up @@ -88,6 +93,11 @@ aciclient.postJson(config)
aciclient.deleteMo('uni/tn-XYZ')
```

### create snapshot
```python
aci.snapshot('test')
```

## Testing

```
Expand All @@ -104,6 +114,7 @@ of conduct, and the process for submitting pull requests to this project.
* **Marcel Zehnder** - *Initial work*
* **Andreas Graber** - *Migration to open source*
* **Richard Strnad** - *Paginagtion for large requests, various small stuff*
* **Dario Kaelin** - *Added snapshot creation*

## License

Expand Down
90 changes: 75 additions & 15 deletions aciClient/aci.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import logging
import json
import requests
import threading

# The modules are named different in python2/python3...
try:
Expand All @@ -27,7 +28,7 @@ class ACI:
# ==============================================================================
# constructor
# ==============================================================================
def __init__(self, apicIp, apicUser, apicPasword):
def __init__(self, apicIp, apicUser, apicPasword, refresh=False):
self.__logger.debug('Constructor called')
self.apicIp = apicIp
self.apicUser = apicUser
Expand All @@ -36,25 +37,36 @@ def __init__(self, apicIp, apicUser, apicPasword):
self.baseUrl = 'https://' + self.apicIp + '/api/'
self.__logger.debug(f'BaseUrl set to: {self.baseUrl}')

self.refresh_auto = refresh
self.refresh_next = None
self.refresh_thread = None
self.refresh_offset = 30
self.session = None
self.token = None

def __refresh_session_timer(self, response):
self.__logger.debug(f'refreshing the token {self.refresh_offset}s before it expires')
self.refresh_next = int(response.json()['imdata'][0]['aaaLogin']['attributes']['refreshTimeoutSeconds'])
self.refresh_thread = threading.Timer(self.refresh_next - self.refresh_offset, self.renewCookie)
self.__logger.debug(f'starting thread to refresh token in {self.refresh_next - self.refresh_offset}s')
self.refresh_thread.start()

# ==============================================================================
# login
# ==============================================================================
def login(self) -> bool:
self.__logger.debug('login called')

self.session = requests.Session()
self.__logger.info('Session Object Created')
self.__logger.debug('Session Object Created')

# create credentials structure
userPass = json.dumps({'aaaUser': {'attributes': {'name': self.apicUser, 'pwd': self.apicPassword}}})

self.__logger.info(f'Login to apic {self.baseUrl}')
response = self.session.post(self.baseUrl + 'aaaLogin.json', data=userPass, verify=False, timeout=5)

# Don't rise an exception for 401
# Don't raise an exception for 401
if response.status_code == 401:
self.__logger.error(f'Login not possible due to Error: {response.text}')
self.session = False
Expand All @@ -64,15 +76,24 @@ def login(self) -> bool:
response.raise_for_status()

self.token = response.json()['imdata'][0]['aaaLogin']['attributes']['token']
self.__logger.info('Successful get Token from APIC')
self.__logger.debug('Successful get Token from APIC')

if self.refresh_auto:
self.__refresh_session_timer(response=response)
return True

# ==============================================================================
# logout
# ==============================================================================
def logout(self):
self.__logger.debug('Logout from APIC...')
self.__logger.debug('logout called')
self.refresh_auto = False
if self.refresh_thread is not None:
if self.refresh_thread.is_alive():
self.__logger.debug('Stoping refresh_auto thread')
self.refresh_thread.cancel()
self.postJson(jsonData={'aaaUser': {'attributes': {'name': self.apicUser}}}, url='aaaLogout.json')
self.__logger.debug('Logout from APIC sucessfull')

# ==============================================================================
# renew cookie (aaaRefresh)
Expand All @@ -81,11 +102,17 @@ def renewCookie(self) -> bool:
self.__logger.debug('Renew Cookie called')
response = self.session.post(self.baseUrl + 'aaaRefresh.json', verify=False)

# Raise Exception for an error 4xx and 5xx
response.raise_for_status()

self.token = response.json()['imdata'][0]['aaaLogin']['attributes']['token']
self.__logger.info('Successful renewed the Token')
if response.status_code == 200:
if self.refresh_auto:
self.__refresh_session_timer(response=response)
self.token = response.json()['imdata'][0]['aaaLogin']['attributes']['token']
self.__logger.debug('Successfuly renewed the token')
else:
self.token = False
self.refresh_auto = False
self.__logger.error(f'Could not renew token. {response.text}')
response.raise_for_status()
return False
return True

# ==============================================================================
Expand All @@ -108,10 +135,10 @@ def getJson(self, uri, subscription=False) -> {}:

if response.ok:
responseJson = response.json()
self.__logger.info(f'Successful get Data from APIC: {responseJson}')
self.__logger.debug(f'Successful get Data from APIC: {responseJson}')
if subscription:
subscription_id = responseJson['subscriptionId']
self.__logger.info(f'Returning Subscription Id: {subscription_id}')
self.__logger.debug(f'Returning Subscription Id: {subscription_id}')
return subscription_id
return responseJson['imdata']

Expand All @@ -120,7 +147,7 @@ def getJson(self, uri, subscription=False) -> {}:
self.__logger.error(f'Error 400 during get occured: {resp_text}')
if resp_text == 'Unable to process the query, result dataset is too big':
# Dataset was too big, we try to grab all the data with pagination
self.__logger.info(f'Trying with Pagination, uri: {uri}')
self.__logger.debug(f'Trying with Pagination, uri: {uri}')
return self.getJsonPaged(uri)
return resp_text
else:
Expand Down Expand Up @@ -148,7 +175,7 @@ def getJsonPaged(self, uri) -> {}:

if response.ok:
responseJson = response.json()
self.__logger.info(f'Successful get Data from APIC: {responseJson}')
self.__logger.debug(f'Successful get Data from APIC: {responseJson}')
if responseJson['imdata']:
return_data.extend(responseJson['imdata'])
else:
Expand All @@ -170,7 +197,7 @@ def postJson(self, jsonData, url='mo.json') -> {}:
self.__logger.debug(f'Post Json called data: {jsonData}')
response = self.session.post(self.baseUrl + url, verify=False, data=json.dumps(jsonData, sort_keys=True))
if response.status_code == 200:
self.__logger.info(f'Successful Posted Data to APIC: {response.json()}')
self.__logger.debug(f'Successful Posted Data to APIC: {response.json()}')
return response.status_code
elif response.status_code == 400:
resp_text = '400: ' + response.json()['imdata'][0]['error']['attributes']['text']
Expand All @@ -192,3 +219,36 @@ def deleteMo(self, dn) -> int:
response.raise_for_status()

return response.status_code

# ==============================================================================
# snapshot
# ==============================================================================
def snapshot(self, description="snapshot") -> bool:
self.__logger.debug(f'snapshot called {description}')

json_payload = [
{
"configExportP": {
"attributes": {
"adminSt": "triggered",
"descr": f"by aciClient - {description}",
"dn": "uni/fabric/configexp-aciclient",
"format": "json",
"includeSecureFields": "yes",
"maxSnapshotCount": "global-limit",
"name": "aciclient",
"nameAlias": "",
"snapshot": "yes",
"targetDn": ""
}
}
}
]

response = self.postJson(json_payload)
if response == 200:
self.__logger.debug('snapshot created and triggered')
return True
else:
self.__logger.error(f'snapshot creation not succesfull: {response}')
return False
4 changes: 2 additions & 2 deletions aciClient/aciCertClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def getJson(self, uri) -> {}:
# Raise Exception if http Error occurred
r.raise_for_status()

self.__logger.info(f'Successful get Data from APIC: {r.json()}')
self.__logger.debug(f'Successful get Data from APIC: {r.json()}')
return r.json()['imdata']

# ==============================================================================
Expand All @@ -73,7 +73,7 @@ def postJson(self, jsonData):
r.raise_for_status()

if r.status_code == 200:
self.__logger.info(f'Successful Posted Data to APIC: {r.json()}')
self.__logger.debug(f'Successful Posted Data to APIC: {r.json()}')
return r.status_code
else:
self.__logger.error(f'Error during get occured: {r.json()}')
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
long_description = f.read()

setup(name='aciClient',
version='1.2',
version='1.3',
description='aci communication helper class',
url='http://www.netcloud.ch',
author='mze',
Expand Down
61 changes: 61 additions & 0 deletions test/test_aci.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from aciClient.aci import ACI
import pytest
import time

__BASE_URL = 'testing-apic.ncdev.ch'

Expand Down Expand Up @@ -46,6 +47,40 @@ def test_login_404_exception(requests_mock):
with pytest.raises(RequestException):
resp = aci.login()

def test_login_refresh_ok(requests_mock):
requests_mock.post(f'https://{__BASE_URL}/api/aaaLogin.json', json={'imdata': [
{'aaaLogin': {'attributes': {'refreshTimeoutSeconds': '31', 'token':'tokenxyz'}}}
]})
requests_mock.post(f'https://{__BASE_URL}/api/aaaRefresh.json', json={
'imdata': [
{
'aaaLogin': {
'attributes': {
'refreshTimeoutSeconds': '300',
'token':'tokenabc'
}
}
}
]})
requests_mock.post(f'https://{__BASE_URL}/api/aaaLogout.json', json={'imdata': []}, status_code=200)
aci = ACI(apicIp=__BASE_URL, apicUser='admin', apicPasword='unkown', refresh=True)
aci.login()
token = aci.getToken()
time.sleep(2)
aci.logout()
assert token != aci.getToken()

def test_login_refresh_nok(requests_mock):
requests_mock.post(f'https://{__BASE_URL}/api/aaaLogin.json', json={'imdata': [
{'aaaLogin': {'attributes': {'refreshTimeoutSeconds': '31', 'token':'tokenxyz'}}}
]})
requests_mock.post(f'https://{__BASE_URL}/api/aaaRefresh.json', json={
'imdata': []}, status_code=403)
aci = ACI(apicIp=__BASE_URL, apicUser='admin', apicPasword='unkown', refresh=True)
aci.login()
time.sleep(3)
token = aci.getToken()
assert not token

def test_renew_cookie_ok(requests_mock):
requests_mock.post(f'https://{__BASE_URL}/api/aaaLogin.json', json={'imdata': [
Expand Down Expand Up @@ -194,3 +229,29 @@ def test_post_tenant_forbidden_exception(requests_mock):
aci.login()
with pytest.raises(RequestException):
aci.postJson(post_data)


def test_snapshot_ok(requests_mock):
requests_mock.post(f'https://{__BASE_URL}/api/mo.json', json={"totalCount": "0", "imdata": []})
requests_mock.post(f'https://{__BASE_URL}/api/aaaLogin.json', json={'imdata': [
{'aaaLogin': {'attributes': {'token': 'tokenxyz'}}}
]})

aci = ACI(apicIp=__BASE_URL, apicUser='admin', apicPasword='unkown')
aci.login()
resp = aci.snapshot(description='unit_test')
assert resp


def test_snapshot_nok(requests_mock):
requests_mock.post(f'https://{__BASE_URL}/api/mo.json',
json={"totalCount": "0", "imdata": [{"error": {"attributes": {"text": "Error UnitTest"}}}]},
status_code=400)
requests_mock.post(f'https://{__BASE_URL}/api/aaaLogin.json', json={'imdata': [
{'aaaLogin': {'attributes': {'token': 'tokenxyz'}}}
]})

aci = ACI(apicIp=__BASE_URL, apicUser='admin', apicPasword='unkown')
aci.login()
resp = aci.snapshot(description='unit_test')
assert not resp