1
0
Fork 0

Compare commits

..

No commits in common. "661e089729c72e3972f1cdb2b7dc084d532a9720" and "d2e39936a09ef4e5750929dbec154c5f1542eee4" have entirely different histories.

13 changed files with 497 additions and 609 deletions

View file

@ -49,9 +49,7 @@ coverage_report:
pep8:
-pep8 -r --max-line-length=120 --ignore=$(PEP8_IGNORE) cvprac/
-pep8 -r --max-line-length=120 --ignore=$(PEP8_IGNORE),E402 test/lib/
-pep8 -r --max-line-length=120 --ignore=$(PEP8_IGNORE),E402 test/system/
-pep8 -r --ignore=$(PEP8_IGNORE),E402,E501 test/unit/
-pep8 -r --max-line-length=120 --ignore=$(PEP8_IGNORE),E402 test/
pyflakes:
pyflakes cvprac/ test/

View file

@ -4,31 +4,25 @@
## Table of Contents
- [Arista Cloudvision® Portal RESTful API Client](#arista-cloudvision-portal-restful-api-client)
- [Table of Contents](#table-of-contents)
- [Overview](#overview)
1. [Overview](#overview)
- [Requirements](#requirements)
- [Installation](#installation)
1. [Installation](#installation)
- [Development: Run from Source](#development-run-from-source)
- [Step 1: Clone the cvprac Github repo](#step-1-clone-the-cvprac-github-repo)
- [Step 2: Check out the desired version or branch](#step-2-check-out-the-desired-version-or-branch)
- [Step 3: Install cvprac using Pip with -e switch](#step-3-install-cvprac-using-pip-with--e-switch)
- [Step 4: Install cvprac development requirements](#step-4-install-cvprac-development-requirements)
- [Getting Started](#getting-started)
1. [Getting Started](#getting-started)
- [Connecting](#connecting)
- [CVP On Premises](#cvp-on-premises)
- [CVaaS](#cvaas)
- [CVP Version Handling](#cvp-version-handling)
- [Examples](#examples)
- [Notes for API Class Usage](#notes-for-api-class-usage)
1. [Notes For API Class Usage](#notes-for-api-class-usage)
- [Containers](#containers)
- [Testing](#testing)
- [Contact or Questions](#contact-or-questions)
- [Contributing](#contributing)
1. [Testing](#testing)
1. [Contact or Questions](#contact-or-questions)
1. [Contributing](#contributing)
- [Working With Git](#working-with-git)
- [Submitting Pull Requests](#submitting-pull-requests)
- [Pull Request Semantics](#pull-request-semantics)
- [License](#license)
1. [License](#license)
## Overview
@ -157,7 +151,7 @@ examples below demonstrate connecting to CVP On Premises setups.
### CVaaS
CVaaS is CloudVision as a Service. Users with CVaaS must use a REST API
token (service account tokens) for accessing CVP with REST APIs.
token for accessing CVP with REST APIs.
- 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
@ -176,22 +170,6 @@ 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.
Please note that the correct regional URL where the CVaaS tenant is deployed must be used. The following are the
cluster URLs used in production:
| Region | URL |
|--------|-----|
| United States 1a | [www.arista.io](https://www.arista.io) |
| United States 1c| [www.cv-prod-us-central1-c.arista.io](https://www.cv-prod-us-central1-c.arista.io)|
| Canada | [www.cv-prod-na-northeast1-b.arista.io](https://www.cv-prod-na-northeast1-b.arista.io)|
| Europe West 2| [www.cv-prod-euwest-2.arista.io](https://www.cv-prod-euwest-2.arista.io)|
| Japan| [www.cv-prod-apnortheast-1.arista.io](https://www.cv-prod-apnortheast-1.arista.io)|
| Australia | [www.cv-prod-ausoutheast-1.arista.io](https://www.cv-prod-ausoutheast-1.arista.io)|
!!! Warning
URLs without `www` are not supported.
### CVP Version Handling
The CVP RESTful APIs often change between releases of CVP. Cvprac

View file

@ -1 +1 @@
1.4.0
1.3.1

View file

@ -32,5 +32,5 @@
''' RESTful API Client class for Cloudvision(R) Portal
'''
__version__ = '1.4.0'
__version__ = '1.3.1'
__author__ = 'Arista Networks, Inc.'

File diff suppressed because it is too large Load diff

View file

@ -29,9 +29,6 @@
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# pylint: disable=too-many-branches,too-many-statements,too-many-locals,too-many-lines
''' RESTful API Client class for Cloudvision(R) Portal
This module provides a RESTful API client for Cloudvision(R) Portal (CVP)
@ -99,31 +96,25 @@ import json
import logging
from logging.handlers import SysLogHandler
from itertools import cycle
from packaging.version import parse
from pkg_resources import parse_version
import requests
from requests.exceptions import ( # pylint: disable=redefined-builtin
ConnectionError,
HTTPError,
Timeout,
ReadTimeout,
TooManyRedirects,
JSONDecodeError
)
from requests.exceptions import ConnectionError, HTTPError, Timeout, \
ReadTimeout, TooManyRedirects, JSONDecodeError
from cvprac.cvp_api import CvpApi
from cvprac.cvp_client_errors import CvpApiError, CvpLoginError, \
CvpRequestError, CvpSessionLogOutError
class CvpClient():
class CvpClient(object):
''' Use this class to create a persistent connection to CVP.
'''
# pylint: disable=too-many-instance-attributes
# Maximum number of times to retry a get or post to the same
# CVP node.
NUM_RETRY_REQUESTS = 3
LATEST_API_VERSION = 9.0
LATEST_API_VERSION = 8.0
def __init__(self, logger='cvprac', syslog=False, filename=None,
log_level='INFO'):
@ -221,8 +212,7 @@ class CvpClient():
self.version = version
self.log.info('Version %s', version)
# Set apiversion to latest available API version for CVaaS
# Set apiversion to 9.0 for 2023.1.x
# Set apiversion to 8.0 for 2022.1.x - 2022.3.x
# Set apiversion to 8.0 for 2022.1.x
# Set apiversion to 7.0 for 2021.3.x
# Set apiversion to 6.0 for 2021.2.x
# Set apiversion to 5.0 for 2020.2.4 through 2021.1.x
@ -242,37 +232,25 @@ class CvpClient():
' Appending 0. Updated Version String - %s',
".".join(version_components))
full_version = ".".join(version_components)
if parse(full_version) >= parse('2024.1.0'):
self.log.info('Setting API version to v12')
self.apiversion = 12.0
elif parse(full_version) >= parse('2023.3.0'):
self.log.info('Setting API version to v11')
self.apiversion = 11.0
elif parse(full_version) >= parse('2023.2.0'):
self.log.info('Setting API version to v10')
self.apiversion = 10.0
elif parse(full_version) >= parse('2023.1.0'):
self.log.info('Setting API version to v9')
self.apiversion = 9.0
elif parse(full_version) >= parse('2022.1.0'):
if parse_version(full_version) >= parse_version('2022.1.0'):
self.log.info('Setting API version to v8')
self.apiversion = 8.0
elif parse(full_version) >= parse('2021.3.0'):
elif parse_version(full_version) >= parse_version('2021.3.0'):
self.log.info('Setting API version to v7')
self.apiversion = 7.0
elif parse(full_version) >= parse('2021.2.0'):
elif parse_version(full_version) >= parse_version('2021.2.0'):
self.log.info('Setting API version to v6')
self.apiversion = 6.0
elif parse(full_version) >= parse('2020.2.4'):
elif parse_version(full_version) >= parse_version('2020.2.4'):
self.log.info('Setting API version to v5')
self.apiversion = 5.0
elif parse(full_version) >= parse('2020.1.1'):
elif parse_version(full_version) >= parse_version('2020.1.1'):
self.log.info('Setting API version to v4')
self.apiversion = 4.0
elif parse(full_version) >= parse('2019.0.0'):
elif parse_version(full_version) >= parse_version('2019.0.0'):
self.log.info('Setting API version to v3')
self.apiversion = 3.0
elif parse(full_version) >= parse('2018.2.0'):
elif parse_version(full_version) >= parse_version('2018.2.0'):
self.log.info('Setting API version to v2')
self.apiversion = 2.0
else:
@ -396,12 +374,13 @@ class CvpClient():
self.error_msg = '\n'
for _ in range(0, num_nodes):
host = next(self.node_pool)
self.url_prefix = f"https://{host}:{self.port or 443}/web"
self.url_prefix_short = f"https://{host}:{self.port or 443}"
self.url_prefix = ('https://%s:%d/web' % (host, self.port or 443))
self.url_prefix_short = ('https://%s:%d'
% (host, self.port or 443))
error = self._reset_session()
if error is None:
break
self.error_msg += f"{host}: {error}\n"
self.error_msg += '%s: %s\n' % (host, error)
def _reset_session(self):
''' Get a new request session and try logging into the current
@ -445,20 +424,23 @@ class CvpClient():
if 'Unauthorized' in response.reason:
# Check for 'Unauthorized' User error because this is how
# CVP responds to a logged out users requests in 2018.x.
msg = f"{prefix}: Request Error: {response.reason}"
msg = '%s: Request Error: %s' % (prefix, response.reason)
self.log.error(msg)
raise CvpApiError(msg)
if 'User is unauthorized' in response.text:
# Check for 'User is unauthorized' response text because this
# is how CVP responds to a logged out users requests in 2019.x.
msg = f"{prefix}: Request Error: User is unauthorized"
msg = '%s: Request Error: User is unauthorized' % prefix
self.log.error(msg)
raise CvpApiError(msg)
msg = f"{prefix}: Request Error: {response.reason} - {response.text}"
raise CvpRequestError(msg)
else:
msg = '%s: Request Error: %s - %s' % (prefix, response.reason,
response.text)
self.log.error(msg)
raise CvpRequestError(msg)
if 'LOG OUT MESSAGE' in response.text:
msg = f"{prefix}: Request Error: session logged out"
msg = ('%s: Request Error: session logged out' % prefix)
raise CvpSessionLogOutError(msg)
joutput = json_decoder(response.text)
@ -474,9 +456,9 @@ class CvpClient():
# Build the error message from all the errors.
err_msg = error_list[0]
for idx in range(1, len(error_list)):
err_msg = f"{err_msg}\n{error_list[idx]}"
err_msg = '%s\n%s' % (err_msg, error_list[idx])
msg = f"{prefix}: Request Error: {err_msg}"
msg = ('%s: Request Error: %s' % (prefix, err_msg))
self.log.error(msg)
raise CvpApiError(msg)
@ -491,7 +473,8 @@ class CvpClient():
response status is not OK.
'''
if not response.ok:
msg = f"{prefix}: Request Error: {response.reason} - {response.text}"
msg = '%s: Request Error: %s - %s' % (prefix, response.reason,
response.text)
self.log.error(msg)
raise CvpRequestError(msg)
@ -525,7 +508,7 @@ class CvpClient():
self.headers.pop('APP_SESSION_ID', None)
if self.api_token is not None:
return self._set_headers_api_token()
if self.is_cvaas:
elif self.is_cvaas:
raise CvpLoginError('CVaaS only supports API token authentication.'
' Please create an API token and provide it'
' via the api_token parameter in combination'
@ -564,7 +547,7 @@ class CvpClient():
headers=self.headers,
timeout=self.connect_timeout,
verify=self.cert)
self._is_good_response(response, f"Authenticate: {url}")
self._is_good_response(response, 'Authenticate: %s' % url)
self.cookies = response.cookies
self.headers['APP_SESSION_ID'] = response.json()['sessionId']
@ -574,20 +557,10 @@ class CvpClient():
'''
# If using an API token there is no need to run a Login API.
# Simply add the token into the headers or cookies
self.headers['Authorization'] = f"Bearer {self.api_token}"
self.headers['Authorization'] = 'Bearer %s' % self.api_token
# Alternative to adding token to headers it can be added to
# cookies as shown below.
# self.cookies = {'access_token': self.api_token}
url = self.url_prefix_short + '/api/v1/rest/'
response = self.session.get(
url,
cookies=self.cookies,
headers=self.headers,
timeout=self.connect_timeout,
verify=self.cert
)
# Verify that the generic request was successful
self._is_good_response(response, f"Authenticate: {url}")
def logout(self):
'''
@ -599,7 +572,7 @@ class CvpClient():
self.log.info('User logged out.')
self.session = None
else:
err = f"Error trying to logout {response}"
err = 'Error trying to logout %s' % response
self.log.error(err)
def _make_request(self, req_type, url, timeout, data=None,
@ -715,8 +688,8 @@ class CvpClient():
try:
resp_data = response.json()
if (resp_data is not None and 'result' in resp_data and
'/resources/' in full_url):
if (resp_data is not None and 'result' in resp_data
and '/resources/' in full_url):
# Resource APIs use JSON streaming and will return
# multiple JSON objects during GetAll type API
# calls. We are wrapping the multiple objects into
@ -737,11 +710,13 @@ class CvpClient():
err_str)
if 'Extra data' in str(error):
self.log.debug('Found multiple objects or NO objects in'
' response data. Attempt to decode')
'response data. Attempt to decode')
decoded_data = json_decoder(response.text)
return {'data': decoded_data}
self.log.error("Unknown format for JSONDecodeError - %s", err_str)
raise error
else:
self.log.error('Unknown format for JSONDecodeError - %s',
err_str)
raise error
def _send_request(self, req_type, full_url, timeout, data=None,
files=None):
@ -808,7 +783,7 @@ class CvpClient():
timeout=timeout,
verify=self.cert)
else:
fhs = {}
fhs = dict()
fhs['Accept'] = self.headers['Accept']
if 'APP_SESSION_ID' in self.headers:
fhs['APP_SESSION_ID'] = self.headers[
@ -843,7 +818,8 @@ class CvpClient():
continue
try:
self._is_good_response(response, f"{req_type}: {full_url} ")
self._is_good_response(response, '%s: %s ' %
(req_type, full_url))
except CvpSessionLogOutError as error:
self.log.debug(error)
# Retry the request to the same node if there was a CVP session
@ -852,10 +828,11 @@ class CvpClient():
# be retried on the same node.
if req_try + 1 == self.NUM_RETRY_REQUESTS:
raise error
self._reset_session()
if not self.session:
raise error
continue
else:
self._reset_session()
if not self.session:
raise error
continue
except CvpApiError as error:
self.log.debug(error)
if ('Unauthorized' in error.msg or
@ -870,12 +847,14 @@ class CvpClient():
# will be retried on the same node.
if req_try + 1 == self.NUM_RETRY_REQUESTS:
raise error
self._reset_session()
if not self.session:
raise error
continue
# pylint: disable=raising-bad-type
raise error
else:
self._reset_session()
if not self.session:
raise error
continue
else:
# pylint: disable=raising-bad-type
raise error
return response
def get(self, url, timeout=30):

View file

@ -1,5 +1,6 @@
check-manifest
coverage
mock
pdoc
pep8
pyflakes

View file

@ -5,12 +5,10 @@ to help users interact with Arista CloudVision easily and automate the provision
## Table of Contents
- [cvprac labs](#cvprac-labs)
- [Table of Contents](#table-of-contents)
- [Authentication](#authentication)
1. [Authentication](#authentication)
- [Password Authentication](#password-authentication)
- [Service Account Token Authentication](#service-account-token-authentication)
- [Known Limitations](#known-limitations)
1. [Known Limitations](#known-limitations)
## Authentication
@ -62,8 +60,6 @@ clnt = CvpClient()
clnt.connect(nodes=['10.83.13.33'], username='',password='',api_token=token)
```
> Note that for CVaaS the correct regional URL must be used including `www.`. Please refer to the main page's [README.md](../../README.md#cvaas)
## Known Limitations
- for any APIs that interact with EOS devices, the service account name must match the name of the username

View file

@ -7,7 +7,7 @@
import argparse
import ssl
import sys
from packaging.version import parse
from pkg_resources import parse_version
from getpass import getpass
from cvprac.cvp_client import CvpClient
import requests.packages.urllib3
@ -56,7 +56,7 @@ def main():
# Get the current CVP version
cvp_release = clnt.api.get_cvp_info()['version']
if parse(cvp_release) < parse('2020.3.0'):
if parse_version(cvp_release) < parse_version('2020.3.0'):
# For older CVP, we manually trigger a compliance check
try:
clnt.api.check_compliance('root', 'container')

View file

@ -1,23 +0,0 @@
######
v1.3.2
######
2023-12-14
Enhancements
^^^^^^^^^^^^
* Add handling of new password change logout functionality in 2023.1.0. (`254 <https://github.com/aristanetworks/cvprac/pull/254>`_) [`mharista <https://github.com/mharista>`_]
* Add support for config validation during config assign. (`255 <https://github.com/aristanetworks/cvprac/pull/255>`_) [`noredistribution <https://github.com/noredistribution>`_]
* Add support for config validation during config removal. (`256 <https://github.com/aristanetworks/cvprac/pull/256>`_) [`noredistribution <https://github.com/noredistribution>`_]
Fixed
^^^^^
* Add ability to use device_decommissioning for unprovisioned devices. (`253 <https://github.com/aristanetworks/cvprac/pull/253>`_) [`noredistribution <https://github.com/noredistribution>`_]
* Add check to connect() to ensure token works. (`258 <https://github.com/aristanetworks/cvprac/pull/258>`_) [`chetryan <https://github.com/chetryan>`_]
Documentation
^^^^^^^^^^^^^
* Add documentation for CVaaS regional URLs. (`259 <https://github.com/aristanetworks/cvprac/pull/259>`_) [`noredistribution <https://github.com/noredistribution>`_]

View file

@ -1,20 +0,0 @@
######
v1.4.0
######
2024-5-6
Enhancements
^^^^^^^^^^^^
* Move from pkg_resources to packaging for Python 3.12 support. (`271 <https://github.com/aristanetworks/cvprac/pull/271>`_) [`mharista <https://github.com/mharista>`_]
* Add support for searchTopology V3 endpoint. (`275 <https://github.com/aristanetworks/cvprac/pull/275>`_) [`mharista <https://github.com/mharista>`_]
Fixed
^^^^^
* Add missing url encoding for get_user. (`264 <https://github.com/aristanetworks/cvprac/pull/264>`_) [`noredistribution <https://github.com/noredistribution>`_]
* Updated the enrollment endpoint for CVaaS. (`269 <https://github.com/aristanetworks/cvprac/pull/269>`_) [`noredistribution <https://github.com/noredistribution>`_]
* Add timeout to get_configlets_and_mappers(). (`270 <https://github.com/aristanetworks/cvprac/pull/270>`_) [`noredistribution <https://github.com/noredistribution>`_]
* Update setup.py to reference python3 only. (`272 <https://github.com/aristanetworks/cvprac/pull/272>`_) [`mharista <https://github.com/mharista>`_]
* Python3 lint/format fixes. (`273 <https://github.com/aristanetworks/cvprac/pull/273>`_) [`mharista <https://github.com/mharista>`_]

View file

@ -1,2 +1 @@
requests[socks]>=2.27.0
packaging>=23.2

View file

@ -100,13 +100,8 @@ setup(
# Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both.
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
],
# What does your project relate to?
@ -116,7 +111,7 @@ setup(
# your project is installed. For an analysis of "install_requires" vs pip's
# requirements files see:
# https://packaging.python.org/en/latest/requirements.html
install_requires=['requests[socks]>=2.27.0', 'packaging>=23.2'],
install_requires=['requests[socks]>=2.27.0'],
# List additional groups of dependencies here (e.g. development
# dependencies). You can install these using the following syntax,