Skip to content

Commit

Permalink
Merge pull request #9 from dka-li/master
Browse files Browse the repository at this point in the history
Snapshot creation and automatic token exipration handling
  • Loading branch information
andreasgraber authored Sep 1, 2021
2 parents f162ffe + 31d86d5 commit 4f7fb9b
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 20 deletions.
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

0 comments on commit 4f7fb9b

Please sign in to comment.