Skip to content

Commit

Permalink
Merge pull request #147 from aristanetworks/develop
Browse files Browse the repository at this point in the history
Release 1.0.6
  • Loading branch information
mharista authored May 17, 2021
2 parents cb12aeb + de159d4 commit e8900dd
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 87 deletions.
40 changes: 13 additions & 27 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -172,22 +172,20 @@ standard form of connection. Multiple examples below demonstrate connecting to C
CVaaS
-----

CVaaS is CloudVision as a Service. Users with CVaaS have two options for connecting to CVP with REST APIs.
CVaaS is CloudVision as a Service. Users with CVaaS must use a REST API token for accessing CVP with REST APIs.

1. Local CVP users with username/password login.
In the case where users authenticate with CVP (CVaaS) using Oauth a REST API token is required to be generated
and used for running REST APIs. In this case no username/password login is necessary, but the API token
(via api_token parameter) must be provided to cvprac client with the is_cvaas parameter.
In the case that the api_token is used for REST APIs the username and password will be ignored and
the tenant parameter is not needed.

In order to use username/password login with CVaaS the user must be a user locally created within CVP.
This option looks very similar to a connection to an On Premises CVP cluster with a couple other options
(is_cvaas and tenant), required by CVaaS.
An example of a CVaaS connection is shown below.

2. Oauth users with REST API token.

In the case where users authenticate with CVP using Oauth a REST API token is required to be generated and
used for running REST APIs. In this case no login is necessary, but the API token must be provided to
cvprac client with the is_cvaas parameter. In the case that the cvaas_token is used for REST APIs the
username and password will be ignored and the tenant parameter is not needed.

Examples for both types of CVaaS connections are shown below.
Note that the token parameter was previously cvaas_token but this has been converted to api_token because
tokens are also available for usage with On Prem CVP deployments. The api_token parameter name is more
generic in this sense. If you are using the cvaas_token parameter please convert to api_token because the
cvaas_token parameter will be deprecated in the future.


CVP Version Handling
Expand Down Expand Up @@ -235,26 +233,14 @@ Same example as above using the API method:
{u'version': u'2016.1.0'}
>>>

Same example as above but connecting to CVaaS with a local CVP username/password:

::

>>> from cvprac.cvp_client import CvpClient
>>> clnt = CvpClient()
>>> clnt.connect(nodes=['cvaas'], username='cvp_local_user', password='cvp_local_word', is_cvaas=True, tenant='user org/tenant')
>>> result = clnt.api.get_cvp_info()
>>> print result
{u'version': u'cvaas'}
>>>

Same example as above but connecting to CVaaS with a token:
Note that the username and password parameters are required by the connect function but will be ignored when using cvaas_token:
Note that the username and password parameters are required by the connect function but will be ignored when using api_token:

::

>>> from cvprac.cvp_client import CvpClient
>>> clnt = CvpClient()
>>> clnt.connect(nodes=['cvaas'], username='', password='', is_cvaas=True, cvaas_token='user token')
>>> clnt.connect(nodes=['cvaas'], username='', password='', is_cvaas=True, api_token='user token')
>>> result = clnt.api.get_cvp_info()
>>> print result
{u'version': u'cvaas'}
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.0.5
1.0.6
2 changes: 1 addition & 1 deletion cvprac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@
''' RESTful API Client class for Cloudvision(R) Portal
'''

__version__ = '1.0.5'
__version__ = '1.0.6'
__author__ = 'Arista Networks, Inc.'
87 changes: 79 additions & 8 deletions cvprac/cvp_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -997,10 +997,7 @@ def get_configlets_by_device_id(self, mac, start=0, end=0):
configlets (list): The list of configlets applied to the device
'''
self.log.debug('get_configlets_by_device: mac: %s' % mac)
data = self.clnt.get('/provisioning/getConfigletsByNetElementId.do?'
'netElementId=%s&queryParam=&startIndex=%d&'
'endIndex=%d' % (mac, start, end),
timeout=self.request_timeout)
data = self.get_configlets_by_netelement_id(mac, start, end)
return data['configletList']

def add_configlet_builder(self, name, config, draft=False, form=None):
Expand Down Expand Up @@ -1104,6 +1101,65 @@ def update_configlet(self, config, key, name, wait_task_ids=False):
return self.clnt.post('/configlet/updateConfiglet.do', data=body,
timeout=self.request_timeout)

def update_configlet_builder(self, name, key, config, draft=False,
wait_for_task=False):
''' Update an existing configlet builder.
Args:
config (str): Contents of the configlet builder configuration
key: (str): key/id of the configlet builder to be updated
name: (str): name of the configlet builder
draft (boolean): is update a draft
wait_for_task (boolean): wait for task IDs to be generated
'''
data = {
"name": name,
"waitForTaskIds": wait_for_task,
"data": {
"main_script": {
"data": config
}
}
}
debug_str = 'update_configlet_builder:' \
' config: {} key: {} name: {} '
self.log.debug(debug_str.format(config, key, name))
# Update the configlet builder
url_string = '/configlet/updateConfigletBuilder.do?' \
'isDraft={}&id={}&action=save'
return self.clnt.post(url_string.format(draft, key),
data=data, timeout=self.request_timeout)

def update_reconcile_configlet(self, device_mac, config, key, name,
reconciled=False):
''' Update the reconcile configlet.
Args:
device_mac (str): Mac address/Key for device whose reconcile
configlet is being updated
config (str): Reconciled config statements
key (str): Reconcile Configlet key
name (str): Reconcile Configlet name
reconciled (boolean): Wait for task IDs to generate
Returns:
data (dict): Contains success or failure message
'''
log_str = ('update_reconcile_configlet:'
' device_mac: {} config: {} key: {} name: {}')
self.log.debug(log_str.format(device_mac, config, key, name))

url_str = ('/provisioning/updateReconcileConfiglet.do?'
'netElementId={}')
body = {
'config': config,
'key': key,
'name': name,
'reconciled': reconciled,
'unCheckedLines': '',
}
return self.clnt.post(url_str.format(device_mac), data=body,
timeout=self.request_timeout)

def add_note_to_configlet(self, key, note):
''' Add a note to a configlet.
Expand Down Expand Up @@ -1658,8 +1714,23 @@ def delete_container(self, container_name, container_key, parent_name,
'parent: %s parent_key: %s' %
(container_name, container_key, parent_name,
parent_key))
return self._container_op(container_name, container_key, parent_name,
resp = self._container_op(container_name, container_key, parent_name,
parent_key, 'delete')
# As of CVP version 2020.1 the addTempAction.do API endpoint stopped
# raising an Error when attempting to delete a container with children.
# To account for this try to see if the container being deleted
# still exists after the attempted delete. If it still exists
# raise an error similar to how CVP behaved prior to CVP 2020.1
try:
still_exists = self.get_container_by_id(container_key)
except CvpApiError as error:
if 'Invalid Container id' in error.msg:
return resp
else:
raise
if still_exists is not None:
raise CvpApiError('Container was not deleted. Check for children')
return resp

def get_parent_container_for_device(self, device_mac):
''' Add the container to the specified parent.
Expand Down Expand Up @@ -1697,9 +1768,9 @@ def move_device_to_container(self, app_name, device, container,
Ex: {u'data': {u'status': u'success', u'taskIds': []}}
'''
info = '%s moving device %s to container %s' % (app_name,
device['fqdn'],
container['name'])
info = 'Device Add {} to container {} by {}'.format(device['fqdn'],
container['name'],
app_name)
self.log.debug('Attempting to move device %s to container %s'
% (device['fqdn'], container['name']))
if 'parentContainerId' in device:
Expand Down
87 changes: 41 additions & 46 deletions cvprac/cvp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
>>>
'''

import os
import re
import json
import logging
Expand Down Expand Up @@ -288,6 +289,14 @@ def connect(self, nodes, username, password, connect_timeout=10,
if not isinstance(nodes, list):
raise TypeError('nodes argument must be a list')

for idx, _ in enumerate(nodes):
if (os.environ.get('CURRENT_NODE_IP') and
nodes[idx] in ['127.0.0.1', 'localhost']):
# We set this env in script-executor container.
# Mask localhost or 127.0.0.1 with node IP if this
# is called from configlet builder scripts.
nodes[idx] = os.environ.get('CURRENT_NODE_IP')

self.cert = cert
self.nodes = nodes
self.node_cnt = len(nodes)
Expand Down Expand Up @@ -406,7 +415,7 @@ def _is_good_response(self, response, prefix):
msg = ('%s: Request Error: session logged out' % prefix)
raise CvpSessionLogOutError(msg)

joutput = response.json()
joutput = json_decoder(response.text)
err_code_val = self._finditem(joutput, 'errorCode')
if err_code_val:
if 'errorMessage' in joutput:
Expand Down Expand Up @@ -472,7 +481,10 @@ def _login(self):
if self.api_token is not None:
return self._set_headers_api_token()
elif self.is_cvaas:
return self._login_cvaas()
raise CvpLoginError('CVaaS only supports API token authentication.'
' Please create an API token and provide it'
' via the api_token parameter in combination'
' with the is_cvaas parameter')
return self._login_on_prem()

def _login_on_prem(self):
Expand Down Expand Up @@ -512,47 +524,6 @@ def _login_on_prem(self):
self.cookies = response.cookies
self.headers['APP_SESSION_ID'] = response.json()['sessionId']

def _login_cvaas(self):
''' Make a POST request to CVaaS login authentication.
An error can be raised from the post method call or the
_check_response_status method call. Any errors raised would be
a good reason not to use this host.
Raises:
ConnectionError: A ConnectionError is raised if there was a
network problem (e.g. DNS failure, refused connection, etc)
CvpApiError: A CvpApiError is raised if there was a JSON error.
CvpRequestError: A CvpRequestError is raised if the request
is not properly constructed.
CvpSessionLogOutError: A CvpSessionLogOutError is raised if
response from server indicates session was logged out.
HTTPError: A HTTPError is raised if there was an invalid HTTP
response.
ReadTimeout: A ReadTimeout is raised if there was a request
timeout when reading from the connection.
Timeout: A Timeout is raised if there was a request timeout.
TooManyRedirects: A TooManyRedirects is raised if the request
exceeds the configured number of maximum redirections
ValueError: A ValueError is raised when there is no valid
CVP session. This occurs because the previous get or post
request failed and no session could be established to a
CVP node. Destroy the class and re-instantiate.
'''
# For local CVaaS users no token is needed and the local username
# and password can be used with the below Login API.
url = (self.url_prefix_short +
'/api/v1/oauth?provider=local&next=false')
cvaas_auth = {"org": self.tenant,
"name": self.authdata['userId'],
"password": self.authdata['password']}
response = self.session.post(url,
data=json.dumps(cvaas_auth),
headers=self.headers,
timeout=self.connect_timeout,
verify=self.cert)
self._check_response_status(response, 'Authenticate: %s' % url)
self.cookies = response.cookies

def _set_headers_api_token(self):
''' Sets headers with API token instead of making a call to login API.
'''
Expand All @@ -568,7 +539,7 @@ def logout(self):
:return:
'''
response = self.session.post('/login/logout.do')
response = self.post('/login/logout.do')
if response['data'] == 'success':
self.log.info('User logged out.')
self.session = None
Expand Down Expand Up @@ -677,8 +648,14 @@ def _make_request(self, req_type, url, timeout, data=None,
except ValueError as error:
self.log.debug('Error trying to decode request response %s',
error)
self.log.debug('Attempt to return response text')
resp_data = dict(data=response.text)
if 'Extra data' in str(error):
self.log.debug('Found multiple objects in response data.'
'Attempt to decode')
decoded_data = json_decoder(response.text)
resp_data = dict(data=decoded_data)
else:
self.log.debug('Attempt to return response text')
resp_data = dict(data=response.text)
else:
self.log.debug('Received no response for request %s %s',
req_type, url)
Expand Down Expand Up @@ -962,3 +939,21 @@ def _finditem(self, obj, key):
if item is not None:
break
return item


def json_decoder(data):
''' Check for ...
'''
decoder = json.JSONDecoder()
position = 0
decoded_data = []
while True:
try:
obj, position = decoder.raw_decode(data, position)
decoded_data.append(obj)
position += 1
except ValueError:
break
if len(decoded_data) == 1:
return decoded_data[0]
return decoded_data
26 changes: 26 additions & 0 deletions docs/release-notes-1.0.6.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
######
v1.0.6
######

2021-5-17

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

* Started to add api method update_configlet_builder and add test.. (`a32dd7a <https://github.com/aristanetworks/cvprac/commit/a32dd7ae00f73d887eb7ae06635c0102be80945d>`_) [`dbm79 <https://github.com/dbm79>`_]
* Added function and test for API endpoint updateReconcileConfiglet.do. (`7e90de9 <https://github.com/aristanetworks/cvprac/commit/7e90de90c416c7dce750e1e9ae2928794efc2b1f>`_) [`mharista <https://github.com/mharista>`_]

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

* Add client handling for new resource API REST bindings that return multiple objects in response data. (`bea2d28 <https://github.com/aristanetworks/cvprac/commit/bea2d282093ceb10085e158acd76ed20c12ae485>`_) [`mharista <https://github.com/mharista>`_]

Fixed
^^^^^

* Fix client logout function to use cvprac client post function instead of session post function. (`abaf257 <https://github.com/aristanetworks/cvprac/commit/abaf2577afb5b9b5e9d99a6b848ca2e987c22e66>`_) [`mharista <https://github.com/mharista>`_]
* Mask localhost/127.0.0.1 with node ip for cb scripts. (`d45ac6e <https://github.com/aristanetworks/cvprac/commit/d45ac6e06394c05bb4c5584a14f262e3c814eef5>`_) [`Rajat Bajaj <https://github.com>`_]
* Updating info string to tackle backend inconsistent state when moving devices from the Undefined container. (`82ea8b9 <https://github.com/aristanetworks/cvprac/commit/82ea8b922c57bb86719351c55a2f8a671d49e0db>`_) [`noredistribution <https://github.com/noredistribution>`_]
* Remove CVaaS un/pw login. Only API tokens for CVaaS now. (`f9fd6b5 <https://github.com/aristanetworks/cvprac/commit/f9fd6b51698de9afcb6112c0180185a6e76f4e5c>`_) [`mharista <https://github.com/mharista>`_]
* Update redundant functions to self reference. (`0095b00 <https://github.com/aristanetworks/cvprac/commit/0095b0001839723680caea62323cae56a130ad32>`_) [`mharista <https://github.com/mharista>`_]
* Add exception when attempting to delete container with children for CVP versions 2020.1 and beyond. (`35bb566 <https://github.com/aristanetworks/cvprac/commit/35bb56609d6d986b11dff11b4454e2cdc120ccd9>`_) [`mharista <https://github.com/mharista>`_]
Loading

0 comments on commit e8900dd

Please sign in to comment.