Skip to content

Commit

Permalink
Merge pull request #262 from aristanetworks/release-1.3.2
Browse files Browse the repository at this point in the history
Release 1.3.2
  • Loading branch information
mharista authored Dec 14, 2023
2 parents 49fbb4a + be33f01 commit d5c692b
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 149 deletions.
40 changes: 31 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,31 @@

## Table of Contents

1. [Overview](#overview)
- [Arista Cloudvision® Portal RESTful API Client](#arista-cloudvision-portal-restful-api-client)
- [Table of Contents](#table-of-contents)
- [Overview](#overview)
- [Requirements](#requirements)
1. [Installation](#installation)
- [Installation](#installation)
- [Development: Run from Source](#development-run-from-source)
1. [Getting Started](#getting-started)
- [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)
- [Connecting](#connecting)
- [CVP On Premises](#cvp-on-premises)
- [CVaaS](#cvaas)
- [CVP Version Handling](#cvp-version-handling)
- [Examples](#examples)
1. [Notes For API Class Usage](#notes-for-api-class-usage)
- [Notes for API Class Usage](#notes-for-api-class-usage)
- [Containers](#containers)
1. [Testing](#testing)
1. [Contact or Questions](#contact-or-questions)
1. [Contributing](#contributing)
- [Testing](#testing)
- [Contact or Questions](#contact-or-questions)
- [Contributing](#contributing)
- [Working With Git](#working-with-git)
- [Submitting Pull Requests](#submitting-pull-requests)
- [Pull Request Semantics](#pull-request-semantics)
1. [License](#license)
- [License](#license)

## Overview

Expand Down Expand Up @@ -151,7 +157,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 for accessing CVP with REST APIs.
token (service account tokens) 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
Expand All @@ -170,6 +176,22 @@ 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
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.3.1
1.3.2
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.3.1'
__version__ = '1.3.2'
__author__ = 'Arista Networks, Inc.'
61 changes: 49 additions & 12 deletions cvprac/cvp_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -572,7 +572,7 @@ def get_configlet_history(self, key, start=0, end=0):
'%s&queryparam=&startIndex=%d&endIndex=%d' %
(key, start, end), timeout=self.request_timeout)

def get_inventory(self, start=0, end=0, query=''):
def get_inventory(self, start=0, end=0, query='', provisioned=True):
''' Returns the a dict of the net elements known to CVP.
Args:
Expand All @@ -595,7 +595,7 @@ def get_inventory(self, start=0, end=0, query=''):
timeout=self.request_timeout)
return data['netElementList']
self.log.debug('v2 Inventory API Call')
data = self.clnt.get('/inventory/devices?provisioned=true',
data = self.clnt.get('/inventory/devices?provisioned=%s' % provisioned,
timeout=self.request_timeout)
containers = self.get_containers()
for dev in data:
Expand Down Expand Up @@ -1312,12 +1312,12 @@ def add_note_to_configlet(self, key, note):

def sanitize_warnings(self, data):
''' Sanitize the warnings returned after validation.
In some cases where the configlets has both errors
and warnings, CVP may split any warnings that have
and warnings, CVP may split any warnings that have
`,` across multiple strings.
This method concats the strings back into one string
per warning, and correct the warningCount.
per warning, and correct the warningCount.
Args:
data (dict): A dict that contians the result
Expand All @@ -1330,11 +1330,11 @@ def sanitize_warnings(self, data):
# nothing to do here, we can return as is
return data
# Since there may be warnings incorrectly split on
# ', ' within the warning text by CVP, we join all the
# ', ' within the warning text by CVP, we join all the
# warnings together using ', ' into one large string
temp_warnings = ", ".join(data['warnings']).strip()

# To split the large string again we match on the
# To split the large string again we match on the
# 'at line XXX' that should indicate the end of the warning.
# We capture as well the remaining \\n or whitespace and include
# the extra ', ' added in the previous step in the matching criteria.
Expand Down Expand Up @@ -1463,7 +1463,7 @@ def _save_topology_v2(self, data):
return self.clnt.post(url, data=data, timeout=self.request_timeout)

def apply_configlets_to_device(self, app_name, dev, new_configlets,
create_task=True, reorder_configlets=False):
create_task=True, reorder_configlets=False, validate=False):
''' Apply the configlets to the device.
Args:
Expand All @@ -1484,6 +1484,12 @@ def apply_configlets_to_device(self, app_name, dev, new_configlets,
directly. Set this parameter to True only with the full
list of configlets being applied to the device provided
via the new_configlets parameter.
validate (bool): Defaults to False. If set to True, the function
will validate and compare the configlets to be attached and
populate the configCompareCount field in the data dict. In case
all keys are 0, ie there is no difference between designed-config
and running-config after applying the configlets, no task will be
generated.
Returns:
response (dict): A dict that contains a status and a list of
Expand Down Expand Up @@ -1536,6 +1542,16 @@ def apply_configlets_to_device(self, app_name, dev, new_configlets,
'nodeTargetIpAddress': dev['ipAddress'],
'childTasks': [],
'parentTask': ''}]}
if validate:
validation_result = self.validate_configlets_for_device(dev['systemMacAddress'], ckeys)
data['data'][0].update({
"configCompareCount": {
"mismatch": validation_result['mismatch'],
"reconcile": validation_result['reconcile'],
"new": validation_result['new']
}
}
)
self.log.debug('apply_configlets_to_device: saveTopology data:\n%s' %
data['data'])
self._add_temp_action(data)
Expand All @@ -1545,7 +1561,7 @@ def apply_configlets_to_device(self, app_name, dev, new_configlets,

# pylint: disable=too-many-locals
def remove_configlets_from_device(self, app_name, dev, del_configlets,
create_task=True):
create_task=True, validate=False):
''' Remove the configlets from the device.
Args:
Expand All @@ -1554,6 +1570,12 @@ def remove_configlets_from_device(self, app_name, dev, del_configlets,
del_configlets (list): List of configlet name and key pairs
create_task (bool): Determines whether or not to execute a save
and create the tasks (if any)
validate (bool): Defaults to False. If set to True, the function
will validate and compare the configlets to be attached and
populate the configCompareCount field in the data dict. In case
all keys are 0, ie there is no difference between designed-config
and running-config after applying the configlets, no task will be
generated.
Returns:
response (dict): A dict that contains a status and a list of
Expand Down Expand Up @@ -1612,6 +1634,16 @@ def remove_configlets_from_device(self, app_name, dev, del_configlets,
'nodeTargetIpAddress': dev['ipAddress'],
'childTasks': [],
'parentTask': ''}]}
if validate:
validation_result = self.validate_configlets_for_device(dev['systemMacAddress'], keep_keys)
data['data'][0].update({
"configCompareCount": {
"mismatch": validation_result['mismatch'],
"reconcile": validation_result['reconcile'],
"new": validation_result['new']
}
}
)
self.log.debug('remove_configlets_from_device: saveTopology data:\n%s'
% data['data'])
self._add_temp_action(data)
Expand Down Expand Up @@ -2952,7 +2984,7 @@ def reset_device(self, app_name, device, create_task=True):
from_id = parent_cont['key']
else:
from_id = ''

data = {'data': [{'info': info,
'infoPreview': info,
'action': 'reset',
Expand Down Expand Up @@ -3775,8 +3807,13 @@ def device_decommissioning(self, device_id, request_id):
'deviceId': 'BAD032986065E8DC14CBB6472EC314A6'},
'time': '2022-02-12T02:58:30.765459650Z'}
'''
device_info = self.get_device_by_serial(device_id)
if device_info is not None and 'serialNumber' in device_info:
device_exists = False
inventory = self.get_inventory(provisioned=False)
for device in inventory:
if device['serialNumber'] == device_id:
device_exists = True
break
if device_exists:
msg = 'Decommissioning via Resource APIs are supported from 2021.3.0 or newer.'
# For on-prem check the version as it is only supported from 2021.3.0+
if self.cvp_version_compare('>=', 7.0, msg):
Expand Down
20 changes: 16 additions & 4 deletions cvprac/cvp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ class CvpClient(object):
# Maximum number of times to retry a get or post to the same
# CVP node.
NUM_RETRY_REQUESTS = 3
LATEST_API_VERSION = 8.0
LATEST_API_VERSION = 9.0

def __init__(self, logger='cvprac', syslog=False, filename=None,
log_level='INFO'):
Expand Down Expand Up @@ -212,7 +212,8 @@ def set_version(self, version):
self.version = version
self.log.info('Version %s', version)
# Set apiversion to latest available API version for CVaaS
# Set apiversion to 8.0 for 2022.1.x
# Set apiversion to 9.0 for 2023.1.x
# Set apiversion to 8.0 for 2022.1.x - 2022.3.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
Expand All @@ -232,7 +233,10 @@ def set_version(self, version):
' Appending 0. Updated Version String - %s',
".".join(version_components))
full_version = ".".join(version_components)
if parse_version(full_version) >= parse_version('2022.1.0'):
if parse_version(full_version) >= parse_version('2023.1.0'):
self.log.info('Setting API version to v9')
self.apiversion = 9.0
elif parse_version(full_version) >= parse_version('2022.1.0'):
self.log.info('Setting API version to v8')
self.apiversion = 8.0
elif parse_version(full_version) >= parse_version('2021.3.0'):
Expand Down Expand Up @@ -561,6 +565,14 @@ def _set_headers_api_token(self):
# 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, 'Authenticate: %s' % url)

def logout(self):
'''
Expand Down Expand Up @@ -710,7 +722,7 @@ def _make_request(self, req_type, url, timeout, data=None,
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}
else:
Expand Down
8 changes: 6 additions & 2 deletions docs/labs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ to help users interact with Arista CloudVision easily and automate the provision

## Table of Contents

1. [Authentication](#authentication)
- [cvprac labs](#cvprac-labs)
- [Table of Contents](#table-of-contents)
- [Authentication](#authentication)
- [Password Authentication](#password-authentication)
- [Service Account Token Authentication](#service-account-token-authentication)
1. [Known Limitations](#known-limitations)
- [Known Limitations](#known-limitations)

## Authentication

Expand Down Expand Up @@ -60,6 +62,8 @@ 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
Expand Down
23 changes: 23 additions & 0 deletions docs/release-notes-1.3.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
######
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>`_]
4 changes: 4 additions & 0 deletions test/fixtures/cvp_nodes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@
username: CvpRacTest
password: AristaInnovates
device: python-test-2
# The below fields can be defined to test the token test cases.
# If they are undefined, the tests are skipped
# api_token: <token here>
# api_token_expired: <expired token here>
38 changes: 21 additions & 17 deletions test/system/test_cvp_change_control_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,24 +412,28 @@ def test_api_change_control_start_invalid_tasks(self):
if self.get_version():
pprint('STARTING CHANGE CONTROL FOR INVALID TASKS...')
# Start the change control
dut = self.duts[0]
node = dut['node'] + ":443"
# dut = self.duts[0]
# node = dut['node'] + ":443"
# CVP 2022.1.0 format The forward slashes in the error string are likely a bug
pprint('SETTING DEFAULT ERROR MESSAGE FORMAT FOR CVP 2022.1.0')
err_msg = 'POST: https://' + node + '/api/resources/changecontrol/v1/' \
'ChangeControlConfig : Request Error:' \
' Not Found - {"code":5, "message":"change' \
' control with ID' \
' \\\\"InvalidCVPRACSystestCCID\\\\"' \
' does not exist"}'
if self.clnt.apiversion < 8.0:
# CVP 2021.X.X format
pprint('USING ERROR MESSAGE FORMAT FOR CVP 2021.X.X')
err_msg = "POST: https://" + node + "/api/resources/changecontrol/v1/" \
"ChangeControlConfig : Request Error: " \
"Bad Request -" \
" {\"code\":9,[ ]?\"message\":\"not approved\"}"
with self.assertRaisesRegex(CvpRequestError, err_msg):
# This format fluctuates between CVP 2022.X.X versions so for now we will remove
# the matching of the exact error message format until better version checking
# granularity is implemented.
# pprint('SETTING DEFAULT ERROR MESSAGE FORMAT FOR CVP 2022.1.0')
# err_msg = 'POST: https://' + node + '/api/resources/changecontrol/v1/' \
# 'ChangeControlConfig : Request Error:' \
# ' Not Found - {"code":5, "message":"change' \
# ' control with ID' \
# ' \\\\"InvalidCVPRACSystestCCID\\\\"' \
# ' does not exist"}'
# if self.clnt.apiversion < 8.0:
# # CVP 2021.X.X format
# pprint('USING ERROR MESSAGE FORMAT FOR CVP 2021.X.X')
# err_msg = "POST: https://" + node + "/api/resources/changecontrol/v1/" \
# "ChangeControlConfig : Request Error: " \
# "Bad Request -" \
# " {\"code\":9,[ ]?\"message\":\"not approved\"}"
# with self.assertRaisesRegex(CvpRequestError, err_msg):
with self.assertRaises(CvpRequestError):
self.start_change_control(CHANGE_CONTROL_ID_INVALID)

def test_api_change_control_delete_invalid_cc(self):
Expand Down
Loading

0 comments on commit d5c692b

Please sign in to comment.