cvprac 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cvprac-2.2.0 → cvprac-2.3.0}/PKG-INFO +15 -24
- {cvprac-2.2.0 → cvprac-2.3.0}/README.md +13 -22
- cvprac-2.3.0/VERSION +1 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac/__init__.py +1 -1
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac/cvp_api.py +28 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac/cvp_client.py +132 -4
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/PKG-INFO +15 -24
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/SOURCES.txt +2 -0
- cvprac-2.3.0/docs/release-notes-2.3.0.rst +17 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/test/fixtures/cvp_nodes.yaml +1 -1
- cvprac-2.3.0/test/fixtures/image-file.swix +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/test/system/test_cvp_client_api.py +9 -2
- {cvprac-2.2.0 → cvprac-2.3.0}/test/unit/test_client.py +185 -3
- cvprac-2.2.0/VERSION +0 -1
- {cvprac-2.2.0 → cvprac-2.3.0}/.pylintrc +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/LICENSE +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/MANIFEST.in +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/Makefile +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac/cvp_client_errors.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/dependency_links.txt +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/requires.txt +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/top_level.txt +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.spec +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/dev-requirements.txt +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/README.md +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab01-cvp-info/get_cvp_info.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/compliance_check.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/get_running_configs.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_all_devices_legacy.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_and_decommission_device.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_devices_from_container_legacy.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_devices_legacy.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/assign_configlet_to_device.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/backup_configlets.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/backup_configletsV2.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/common.cfg +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/config_search.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/configlet_list.txt +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/create_configlet.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/create_configlet_from_file.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/get_applied_netelements.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/get_configlets.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/reorder_configlet_on_device.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/update_configlet.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/add_image_to_container.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/assign_configlet_to_container.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/create_container.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/remove_image_from_container.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/rename_container.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab05-device-management/add_image_to_devices.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab05-device-management/add_image_wo_tempaction.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab05-device-management/remove_image_from_device.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab05-device-management/set_mgmt_ip.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/atd_e2e_provisioning_workflow.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/auto_reconcile_on_rc_change.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/change_control_custom_rapi.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/change_control_workflow_rapi.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf1.cfg +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf2.cfg +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf3.cfg +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf4.cfg +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_spine1.cfg +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_spine2.cfg +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/gen_builder.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/mlag_issu.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/move_device.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/vc_task_retrigger.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/aaa_users.csv +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/add_new_user_cvaas.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/add_new_user_onprem.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/add_users_from_csv_cvaas.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/create_svc_account.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/create_svc_account_token.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/create_terminattr_tokens.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/cvaas.tok +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/delete_all_expired_svc_account_tokens.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/delete_svc_account.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/delete_svc_account_created_by_user.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/get_user_info.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/svc_account_misc.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab08-resource-apis/resource_cvprac.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab08-resource-apis/topology_tag_assignment.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/static/serviceaccount1.png +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/static/serviceaccount2.png +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/static/serviceaccount3.png +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-0.7.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-0.8.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-0.9.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.1.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.2.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.3.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.4.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.5.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.6.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.7.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.2.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.2.2.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.3.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.3.1.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.3.2.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.4.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.4.1.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.4.2.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-2.0.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-2.1.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-2.2.0.rst +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/requirements.txt +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/setup.cfg +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/setup.py +0 -0
- /cvprac-2.2.0/test/fixtures/image-file.swix → /cvprac-2.3.0/test/fixtures/image-file-1787075019.168597.swix +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/test/lib/systestlib.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/test/system/test_cvp_base.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/test/system/test_cvp_change_control_api.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/test/system/test_cvp_client.py +0 -0
- {cvprac-2.2.0 → cvprac-2.3.0}/test/unit/test_api.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cvprac
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Arista Cloudvision(R) Portal Rest API Client written in python
|
|
5
5
|
Home-page: https://github.com/aristanetworks/cvprac
|
|
6
|
-
Download-URL: https://github.com/aristanetworks/cvprac/tarball/2.
|
|
6
|
+
Download-URL: https://github.com/aristanetworks/cvprac/tarball/2.3.0
|
|
7
7
|
Author: Arista Networks, Inc.
|
|
8
8
|
Author-email: eosplus-dev@arista.com
|
|
9
9
|
License: BSD-3
|
|
@@ -296,6 +296,17 @@ Same example as above using the API method:
|
|
|
296
296
|
{u'version': u'2016.1.0'}
|
|
297
297
|
>>>
|
|
298
298
|
|
|
299
|
+
Example using CVP On Prem client cert login:
|
|
300
|
+
|
|
301
|
+
>>> from cvprac.cvp_client import CvpClient
|
|
302
|
+
>>> clnt = CvpClient()
|
|
303
|
+
>>> cert, key = "/cert/client.crt", "/cert/client.key" # cert_file_path, private_key_file_path
|
|
304
|
+
>>> clnt.connect(['cvp1', 'cvp2', 'cvp3'], 'cvp_user', 'cvp_word', cert_login=True, client_cert=(cert, key))
|
|
305
|
+
>>> result = clnt.get('/cvpInfo/getCvpInfo.do')
|
|
306
|
+
>>> print result
|
|
307
|
+
{u'version': u'2016.1.0'}
|
|
308
|
+
>>>
|
|
309
|
+
|
|
299
310
|
Same example as above but connecting to CVaaS with a token: Note that
|
|
300
311
|
the username and password parameters are required by the connect
|
|
301
312
|
function but will be ignored when using api\_token:
|
|
@@ -336,28 +347,8 @@ requiring a manual refresh.
|
|
|
336
347
|
|
|
337
348
|
## Testing
|
|
338
349
|
|
|
339
|
-
The cvprac module provides
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
Requirements for running the system tests:
|
|
343
|
-
|
|
344
|
-
- Need one CVP node for test with a test user account. Create the same
|
|
345
|
-
account on the switch used for testing. The user account information
|
|
346
|
-
follows:
|
|
347
|
-
|
|
348
|
-
username: CvpRacTest
|
|
349
|
-
password: AristaInnovates
|
|
350
|
-
|
|
351
|
-
If switch does not have correct username and/or password then the tests that
|
|
352
|
-
execute tasks will fail with the following error:
|
|
353
|
-
|
|
354
|
-
AssertionError: Execution for task id 220 failed and in the test log is the error:
|
|
355
|
-
|
|
356
|
-
Failure response received from the netElement : ' Unauthorized User '
|
|
357
|
-
|
|
358
|
-
- Test has dedicated access to the CVP node.
|
|
359
|
-
- CVP node contains at least one device in a container.
|
|
360
|
-
- Container or device has at least one configlet applied.
|
|
350
|
+
The cvprac module provides both unit tests and system tests.
|
|
351
|
+
To run the system tests, read the [detailed system test documentation](test/system/SYSTEM_TEST_CVP_SETUP.md) for more information on how to set up the environment.
|
|
361
352
|
|
|
362
353
|
To run the system tests:
|
|
363
354
|
|
|
@@ -247,6 +247,17 @@ Same example as above using the API method:
|
|
|
247
247
|
{u'version': u'2016.1.0'}
|
|
248
248
|
>>>
|
|
249
249
|
|
|
250
|
+
Example using CVP On Prem client cert login:
|
|
251
|
+
|
|
252
|
+
>>> from cvprac.cvp_client import CvpClient
|
|
253
|
+
>>> clnt = CvpClient()
|
|
254
|
+
>>> cert, key = "/cert/client.crt", "/cert/client.key" # cert_file_path, private_key_file_path
|
|
255
|
+
>>> clnt.connect(['cvp1', 'cvp2', 'cvp3'], 'cvp_user', 'cvp_word', cert_login=True, client_cert=(cert, key))
|
|
256
|
+
>>> result = clnt.get('/cvpInfo/getCvpInfo.do')
|
|
257
|
+
>>> print result
|
|
258
|
+
{u'version': u'2016.1.0'}
|
|
259
|
+
>>>
|
|
260
|
+
|
|
250
261
|
Same example as above but connecting to CVaaS with a token: Note that
|
|
251
262
|
the username and password parameters are required by the connect
|
|
252
263
|
function but will be ignored when using api\_token:
|
|
@@ -287,28 +298,8 @@ requiring a manual refresh.
|
|
|
287
298
|
|
|
288
299
|
## Testing
|
|
289
300
|
|
|
290
|
-
The cvprac module provides
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
Requirements for running the system tests:
|
|
294
|
-
|
|
295
|
-
- Need one CVP node for test with a test user account. Create the same
|
|
296
|
-
account on the switch used for testing. The user account information
|
|
297
|
-
follows:
|
|
298
|
-
|
|
299
|
-
username: CvpRacTest
|
|
300
|
-
password: AristaInnovates
|
|
301
|
-
|
|
302
|
-
If switch does not have correct username and/or password then the tests that
|
|
303
|
-
execute tasks will fail with the following error:
|
|
304
|
-
|
|
305
|
-
AssertionError: Execution for task id 220 failed and in the test log is the error:
|
|
306
|
-
|
|
307
|
-
Failure response received from the netElement : ' Unauthorized User '
|
|
308
|
-
|
|
309
|
-
- Test has dedicated access to the CVP node.
|
|
310
|
-
- CVP node contains at least one device in a container.
|
|
311
|
-
- Container or device has at least one configlet applied.
|
|
301
|
+
The cvprac module provides both unit tests and system tests.
|
|
302
|
+
To run the system tests, read the [detailed system test documentation](test/system/SYSTEM_TEST_CVP_SETUP.md) for more information on how to set up the environment.
|
|
312
303
|
|
|
313
304
|
To run the system tests:
|
|
314
305
|
|
cvprac-2.3.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2.3.0
|
|
@@ -2312,6 +2312,34 @@ class CvpApi():
|
|
|
2312
2312
|
files={'file': image_data})
|
|
2313
2313
|
return response
|
|
2314
2314
|
|
|
2315
|
+
def image_upload(self, name, filepath, rebootRequired=False):
|
|
2316
|
+
''' Add an image to a CVP cluster studios image repository.
|
|
2317
|
+
|
|
2318
|
+
Args:
|
|
2319
|
+
name (str): The name that will be used to identify the image
|
|
2320
|
+
or extension in CloudVision. Must have supported extension
|
|
2321
|
+
type such as swi or swix.
|
|
2322
|
+
filepath (str): Local path to the image to upload.
|
|
2323
|
+
rebootRequired (bool): Specifies if the image or extension requires
|
|
2324
|
+
a device reboot. Reboots are always required for .swi files.
|
|
2325
|
+
|
|
2326
|
+
Returns:
|
|
2327
|
+
data (dict): Dictionary of image add data.
|
|
2328
|
+
'''
|
|
2329
|
+
# Get the absolute file path to be uploaded
|
|
2330
|
+
image_path = os.path.abspath(filepath)
|
|
2331
|
+
data = {
|
|
2332
|
+
'name': name,
|
|
2333
|
+
'rebootRequired': rebootRequired,
|
|
2334
|
+
}
|
|
2335
|
+
with open(image_path, 'rb') as image_data:
|
|
2336
|
+
response = self.clnt.post(
|
|
2337
|
+
'/cvpservice/softwaremanagement/v1/uploads',
|
|
2338
|
+
data=data,
|
|
2339
|
+
files={'file': image_data},
|
|
2340
|
+
)
|
|
2341
|
+
return response
|
|
2342
|
+
|
|
2315
2343
|
def cancel_image(self, image_name):
|
|
2316
2344
|
''' Discard/cancel the uploaded image/image bundle before save.
|
|
2317
2345
|
|
|
@@ -96,10 +96,12 @@ Example:
|
|
|
96
96
|
import os
|
|
97
97
|
import re
|
|
98
98
|
import json
|
|
99
|
+
import ssl
|
|
99
100
|
import logging
|
|
100
101
|
from logging.handlers import SysLogHandler
|
|
101
102
|
from itertools import cycle
|
|
102
103
|
from packaging.version import parse
|
|
104
|
+
from urllib.parse import parse_qs, urlparse, urlunparse
|
|
103
105
|
|
|
104
106
|
import requests
|
|
105
107
|
from requests.exceptions import ( # pylint: disable=redefined-builtin
|
|
@@ -116,6 +118,22 @@ from cvprac.cvp_client_errors import CvpApiError, CvpLoginError, \
|
|
|
116
118
|
CvpRequestError, CvpSessionLogOutError
|
|
117
119
|
|
|
118
120
|
|
|
121
|
+
def url_with_port(url, port):
|
|
122
|
+
'''Return url with netloc port replaced by port.
|
|
123
|
+
|
|
124
|
+
For example, url_with_port('https://cvp.example.com:443/web', 9443)
|
|
125
|
+
returns 'https://cvp.example.com:9443/web'.
|
|
126
|
+
'''
|
|
127
|
+
parsed = urlparse(url)
|
|
128
|
+
host = parsed.hostname
|
|
129
|
+
if ':' in host and not host.startswith('['):
|
|
130
|
+
host = f'[{host}]'
|
|
131
|
+
netloc = host
|
|
132
|
+
if port:
|
|
133
|
+
netloc = f'{host}:{port}'
|
|
134
|
+
return urlunparse(parsed._replace(netloc=netloc))
|
|
135
|
+
|
|
136
|
+
|
|
119
137
|
class CvpClient():
|
|
120
138
|
''' Use this class to create a persistent connection to CVP.
|
|
121
139
|
'''
|
|
@@ -142,6 +160,9 @@ class CvpClient():
|
|
|
142
160
|
self.apiversion = None
|
|
143
161
|
self.authdata = None
|
|
144
162
|
self.cert = False
|
|
163
|
+
self.cert_login = False
|
|
164
|
+
self.cert_login_port = None
|
|
165
|
+
self.client_cert = None
|
|
145
166
|
self.connect_timeout = None
|
|
146
167
|
self.cookies = None
|
|
147
168
|
self.error_msg = ''
|
|
@@ -325,7 +346,8 @@ class CvpClient():
|
|
|
325
346
|
def connect(self, nodes, username, password, connect_timeout=10,
|
|
326
347
|
request_timeout=30, protocol='https', port=None, cert=False,
|
|
327
348
|
is_cvaas=False, tenant=None, api_token=None, cvaas_token=None,
|
|
328
|
-
proxies=None
|
|
349
|
+
proxies=None, cert_login=False, client_cert=None,
|
|
350
|
+
cert_login_port=9443):
|
|
329
351
|
''' Login to CVP and get a session ID and cookie. Currently
|
|
330
352
|
certificates are not verified if the https protocol is specified. A
|
|
331
353
|
warning may be printed out from the requests module for this case.
|
|
@@ -366,6 +388,13 @@ class CvpClient():
|
|
|
366
388
|
Proxies can also be set via environment variables.
|
|
367
389
|
Please reference the below link for details of precedence.
|
|
368
390
|
https://requests.readthedocs.io/en/latest/user/advanced/#proxies
|
|
391
|
+
cert_login (boolean): Use CVP certificate based login flow.
|
|
392
|
+
Supported on CVP 2026.3.0 and later. When True,
|
|
393
|
+
client_cert must also be provided.
|
|
394
|
+
client_cert (str or tuple): Path to client certificate, or
|
|
395
|
+
(cert, key) tuple as accepted by requests.
|
|
396
|
+
cert_login_port (int): CVP mTLS endpoint port. Default is
|
|
397
|
+
9443.
|
|
369
398
|
|
|
370
399
|
Raises:
|
|
371
400
|
CvpLoginError: A CvpLoginError is raised if a connection
|
|
@@ -388,6 +417,9 @@ class CvpClient():
|
|
|
388
417
|
nodes[idx] = os.environ.get('CURRENT_NODE_IP')
|
|
389
418
|
|
|
390
419
|
self.cert = cert
|
|
420
|
+
self.cert_login = cert_login
|
|
421
|
+
self.cert_login_port = cert_login_port
|
|
422
|
+
self.client_cert = client_cert
|
|
391
423
|
self.nodes = nodes
|
|
392
424
|
self.node_cnt = len(nodes)
|
|
393
425
|
self.node_pool = cycle(nodes)
|
|
@@ -538,6 +570,86 @@ class CvpClient():
|
|
|
538
570
|
self.log.error(msg)
|
|
539
571
|
raise CvpRequestError(msg)
|
|
540
572
|
|
|
573
|
+
def _check_validate_certificate_result(self, response, prefix):
|
|
574
|
+
'''Check certificate validation details returned in redirect metadata.
|
|
575
|
+
'''
|
|
576
|
+
headers = getattr(response, 'headers', {}) or {}
|
|
577
|
+
location = headers.get('Location') if hasattr(headers, 'get') else ''
|
|
578
|
+
if not isinstance(location, str) or not location:
|
|
579
|
+
return
|
|
580
|
+
query = parse_qs(urlparse(location).query)
|
|
581
|
+
cert_valid = query.get('cert_valid', [None])[0]
|
|
582
|
+
err_msg = query.get('cert_error', [None])[0]
|
|
583
|
+
if err_msg:
|
|
584
|
+
try:
|
|
585
|
+
err_data = json.loads(err_msg)
|
|
586
|
+
except ValueError:
|
|
587
|
+
pass
|
|
588
|
+
else:
|
|
589
|
+
if isinstance(err_data, dict) and err_data.get('errorMessage'):
|
|
590
|
+
err_msg = err_data['errorMessage']
|
|
591
|
+
if not isinstance(err_msg, str):
|
|
592
|
+
err_msg = str(err_msg)
|
|
593
|
+
# Backend returns a generic error message only for browser use case
|
|
594
|
+
# that is not helpful here. If the error message contains the string
|
|
595
|
+
# "close the browser and try again" then remove that part of the
|
|
596
|
+
# message to make it more useful.
|
|
597
|
+
# e.g "x y z. Please close the browser and try again. a b." becomes "x y z"
|
|
598
|
+
# e.g "x y z, then close the browser and try again." becomes "x y z"
|
|
599
|
+
close_browser_msg = 'close the browser and try again'
|
|
600
|
+
err_msg_lower = err_msg.lower()
|
|
601
|
+
close_browser_idx = err_msg_lower.find(close_browser_msg)
|
|
602
|
+
if close_browser_idx != -1:
|
|
603
|
+
end_idx = max(err_msg.rfind('.', 0, close_browser_idx),
|
|
604
|
+
err_msg.rfind(',', 0, close_browser_idx))
|
|
605
|
+
if end_idx != -1:
|
|
606
|
+
err_msg = err_msg[:end_idx]
|
|
607
|
+
err_msg = err_msg.strip() if err_msg else err_msg
|
|
608
|
+
if err_msg or (cert_valid and cert_valid.lower() != 'true'):
|
|
609
|
+
msg = f"{prefix}: Request Error: {err_msg or location}"
|
|
610
|
+
self.log.error(msg)
|
|
611
|
+
raise CvpApiError(msg)
|
|
612
|
+
|
|
613
|
+
def _validate_client_certificate(self):
|
|
614
|
+
'''Validate the local client certificate/key before calling CVP.
|
|
615
|
+
'''
|
|
616
|
+
cert_file = self.client_cert
|
|
617
|
+
key_file = None
|
|
618
|
+
if isinstance(self.client_cert, (tuple)):
|
|
619
|
+
if len(self.client_cert) != 2:
|
|
620
|
+
msg = ('Invalid client certificate/key: client_cert must be a '
|
|
621
|
+
'(certificate, key) pair')
|
|
622
|
+
self.log.error(msg)
|
|
623
|
+
raise CvpRequestError(msg)
|
|
624
|
+
cert_file, key_file = self.client_cert
|
|
625
|
+
|
|
626
|
+
try:
|
|
627
|
+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
628
|
+
context.load_cert_chain(certfile=cert_file, keyfile=key_file)
|
|
629
|
+
except (OSError, ssl.SSLError, TypeError, ValueError) as error:
|
|
630
|
+
msg = "Invalid client certificate or key"
|
|
631
|
+
self.log.error(msg)
|
|
632
|
+
raise CvpRequestError(msg) from error
|
|
633
|
+
|
|
634
|
+
def _validate_certificate(self):
|
|
635
|
+
'''Validate a client certificate for certificate based login.
|
|
636
|
+
'''
|
|
637
|
+
self._validate_client_certificate()
|
|
638
|
+
url = (url_with_port(self.url_prefix_short, self.cert_login_port) +
|
|
639
|
+
'/aaa/v1/validateCertificate')
|
|
640
|
+
response = self.session.get(url, headers=self.headers,
|
|
641
|
+
timeout=self.connect_timeout, verify=self.cert,
|
|
642
|
+
cert=self.client_cert,
|
|
643
|
+
allow_redirects=False)
|
|
644
|
+
prefix = f"Validate certificate: {url}"
|
|
645
|
+
self._is_good_response(response, prefix)
|
|
646
|
+
self._check_validate_certificate_result(response, prefix)
|
|
647
|
+
|
|
648
|
+
if self.cookies is None:
|
|
649
|
+
self.cookies = response.cookies
|
|
650
|
+
else:
|
|
651
|
+
self.cookies.update(response.cookies)
|
|
652
|
+
|
|
541
653
|
def _login(self):
|
|
542
654
|
''' Make a POST request to CVP login authentication.
|
|
543
655
|
An error can be raised from the post method call or the
|
|
@@ -602,14 +714,24 @@ class CvpClient():
|
|
|
602
714
|
CVP node. Destroy the class and re-instantiate.
|
|
603
715
|
'''
|
|
604
716
|
url = self.url_prefix + '/login/authenticate.do'
|
|
717
|
+
if self.cert_login:
|
|
718
|
+
if self.client_cert is None:
|
|
719
|
+
raise CvpRequestError(
|
|
720
|
+
'client_cert is required when cert_login is True')
|
|
721
|
+
self._validate_certificate()
|
|
605
722
|
response = self.session.post(url,
|
|
606
723
|
data=json.dumps(self.authdata),
|
|
607
724
|
headers=self.headers,
|
|
725
|
+
cookies=self.cookies,
|
|
608
726
|
timeout=self.connect_timeout,
|
|
609
|
-
verify=self.cert
|
|
727
|
+
verify=self.cert,
|
|
728
|
+
cert=self.client_cert)
|
|
610
729
|
self._is_good_response(response, f"Authenticate: {url}")
|
|
611
730
|
|
|
612
|
-
self.cookies
|
|
731
|
+
if self.cookies is None:
|
|
732
|
+
self.cookies = response.cookies
|
|
733
|
+
else:
|
|
734
|
+
self.cookies.update(response.cookies)
|
|
613
735
|
self.headers['APP_SESSION_ID'] = response.json()['sessionId']
|
|
614
736
|
|
|
615
737
|
def _set_headers_api_token(self):
|
|
@@ -859,12 +981,18 @@ class CvpClient():
|
|
|
859
981
|
if 'Authorization' in self.headers:
|
|
860
982
|
fhs['Authorization'] = self.headers[
|
|
861
983
|
'Authorization']
|
|
984
|
+
# Data must be None or a dict so the key-value pairs can be extracted
|
|
985
|
+
# as individual multipart/form-data fields right alongside the file
|
|
986
|
+
# boundary parts. Hence when both data= and file= are present we do not
|
|
987
|
+
# want to dump data as a JSON encoded string. Requests cannot merge a
|
|
988
|
+
# raw string body with files and will raise a ValueError.
|
|
862
989
|
response = self.session.post(full_url,
|
|
863
990
|
cookies=self.cookies,
|
|
864
991
|
headers=fhs,
|
|
865
992
|
timeout=timeout,
|
|
866
993
|
verify=self.cert,
|
|
867
|
-
files=files
|
|
994
|
+
files=files,
|
|
995
|
+
data=data)
|
|
868
996
|
elif req_type == 'DELETE':
|
|
869
997
|
response = self.session.delete(full_url,
|
|
870
998
|
cookies=self.cookies,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cvprac
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Arista Cloudvision(R) Portal Rest API Client written in python
|
|
5
5
|
Home-page: https://github.com/aristanetworks/cvprac
|
|
6
|
-
Download-URL: https://github.com/aristanetworks/cvprac/tarball/2.
|
|
6
|
+
Download-URL: https://github.com/aristanetworks/cvprac/tarball/2.3.0
|
|
7
7
|
Author: Arista Networks, Inc.
|
|
8
8
|
Author-email: eosplus-dev@arista.com
|
|
9
9
|
License: BSD-3
|
|
@@ -296,6 +296,17 @@ Same example as above using the API method:
|
|
|
296
296
|
{u'version': u'2016.1.0'}
|
|
297
297
|
>>>
|
|
298
298
|
|
|
299
|
+
Example using CVP On Prem client cert login:
|
|
300
|
+
|
|
301
|
+
>>> from cvprac.cvp_client import CvpClient
|
|
302
|
+
>>> clnt = CvpClient()
|
|
303
|
+
>>> cert, key = "/cert/client.crt", "/cert/client.key" # cert_file_path, private_key_file_path
|
|
304
|
+
>>> clnt.connect(['cvp1', 'cvp2', 'cvp3'], 'cvp_user', 'cvp_word', cert_login=True, client_cert=(cert, key))
|
|
305
|
+
>>> result = clnt.get('/cvpInfo/getCvpInfo.do')
|
|
306
|
+
>>> print result
|
|
307
|
+
{u'version': u'2016.1.0'}
|
|
308
|
+
>>>
|
|
309
|
+
|
|
299
310
|
Same example as above but connecting to CVaaS with a token: Note that
|
|
300
311
|
the username and password parameters are required by the connect
|
|
301
312
|
function but will be ignored when using api\_token:
|
|
@@ -336,28 +347,8 @@ requiring a manual refresh.
|
|
|
336
347
|
|
|
337
348
|
## Testing
|
|
338
349
|
|
|
339
|
-
The cvprac module provides
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
Requirements for running the system tests:
|
|
343
|
-
|
|
344
|
-
- Need one CVP node for test with a test user account. Create the same
|
|
345
|
-
account on the switch used for testing. The user account information
|
|
346
|
-
follows:
|
|
347
|
-
|
|
348
|
-
username: CvpRacTest
|
|
349
|
-
password: AristaInnovates
|
|
350
|
-
|
|
351
|
-
If switch does not have correct username and/or password then the tests that
|
|
352
|
-
execute tasks will fail with the following error:
|
|
353
|
-
|
|
354
|
-
AssertionError: Execution for task id 220 failed and in the test log is the error:
|
|
355
|
-
|
|
356
|
-
Failure response received from the netElement : ' Unauthorized User '
|
|
357
|
-
|
|
358
|
-
- Test has dedicated access to the CVP node.
|
|
359
|
-
- CVP node contains at least one device in a container.
|
|
360
|
-
- Container or device has at least one configlet applied.
|
|
350
|
+
The cvprac module provides both unit tests and system tests.
|
|
351
|
+
To run the system tests, read the [detailed system test documentation](test/system/SYSTEM_TEST_CVP_SETUP.md) for more information on how to set up the environment.
|
|
361
352
|
|
|
362
353
|
To run the system tests:
|
|
363
354
|
|
|
@@ -39,6 +39,7 @@ docs/release-notes-1.4.2.rst
|
|
|
39
39
|
docs/release-notes-2.0.0.rst
|
|
40
40
|
docs/release-notes-2.1.0.rst
|
|
41
41
|
docs/release-notes-2.2.0.rst
|
|
42
|
+
docs/release-notes-2.3.0.rst
|
|
42
43
|
docs/labs/README.md
|
|
43
44
|
docs/labs/lab01-cvp-info/get_cvp_info.py
|
|
44
45
|
docs/labs/lab02-inventory-operations/compliance_check.py
|
|
@@ -102,6 +103,7 @@ docs/labs/static/serviceaccount1.png
|
|
|
102
103
|
docs/labs/static/serviceaccount2.png
|
|
103
104
|
docs/labs/static/serviceaccount3.png
|
|
104
105
|
test/fixtures/cvp_nodes.yaml
|
|
106
|
+
test/fixtures/image-file-1787075019.168597.swix
|
|
105
107
|
test/fixtures/image-file.swix
|
|
106
108
|
test/lib/systestlib.py
|
|
107
109
|
test/system/test_cvp_base.py
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
######
|
|
2
|
+
v2.3.0
|
|
3
|
+
######
|
|
4
|
+
|
|
5
|
+
2026-9-10
|
|
6
|
+
|
|
7
|
+
Enhancements
|
|
8
|
+
^^^^^^^^^^^^
|
|
9
|
+
|
|
10
|
+
* Allow cert-based login. (`323 <https://github.com/aristanetworks/cvprac/pull/323>`_) [`vaibhavshaw-star <https://github.com/vaibhavshaw-star>`_]
|
|
11
|
+
* Add support for image upload into studios repository. (`331 <https://github.com/aristanetworks/cvprac/pull/331>`_) [`mharista <https://github.com/mharista>`_]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Documentation
|
|
15
|
+
^^^^^^^^^^^^^
|
|
16
|
+
|
|
17
|
+
* Add documentation on system test setup. (`319 <https://github.com/aristanetworks/cvprac/pull/319>`_) [`chetryan <https://github.com/chetryan>`_]
|
|
Binary file
|
|
@@ -35,10 +35,16 @@
|
|
|
35
35
|
|
|
36
36
|
''' System test for the CvpApi class
|
|
37
37
|
|
|
38
|
+
Refer to test/system/SYSTEM_TEST_CVP_SETUP.md for more details.
|
|
39
|
+
|
|
40
|
+
The file test/fixtures/cvp_nodes.yaml should be modified to
|
|
41
|
+
point to the CVP instance under test, with the associated username & password
|
|
42
|
+
and device name.
|
|
43
|
+
|
|
38
44
|
Requirements for CVP Node:
|
|
39
45
|
1) Test has dedicated access to the CVP node.
|
|
40
|
-
2) Contains at least one device in
|
|
41
|
-
3)
|
|
46
|
+
2) Contains at least one device in the "Tenant" container.
|
|
47
|
+
3) Device has at least one configlet applied directly, without Reconcile configlets.
|
|
42
48
|
4) Device has a user account and password that matches the CVP username
|
|
43
49
|
and password. If device does not have correct username and/or password
|
|
44
50
|
then the tests that execute tasks will fail with the following error:
|
|
@@ -48,6 +54,7 @@
|
|
|
48
54
|
and in the test log is the error:
|
|
49
55
|
|
|
50
56
|
Failure response received from the netElement : ' Unauthorized User '
|
|
57
|
+
5) Device needs to have at least 1 interface called Ethernet1
|
|
51
58
|
'''
|
|
52
59
|
import os
|
|
53
60
|
import shutil
|
|
@@ -34,12 +34,15 @@
|
|
|
34
34
|
''' Unit tests for the CvpClient class
|
|
35
35
|
'''
|
|
36
36
|
import json
|
|
37
|
+
import ssl
|
|
37
38
|
import unittest
|
|
38
39
|
from itertools import cycle
|
|
39
|
-
from unittest.mock import Mock
|
|
40
|
+
from unittest.mock import Mock, patch
|
|
41
|
+
from requests.cookies import cookiejar_from_dict
|
|
40
42
|
from requests.exceptions import HTTPError, ReadTimeout, JSONDecodeError
|
|
41
|
-
from cvprac.cvp_client import CvpClient
|
|
42
|
-
from cvprac.cvp_client_errors import CvpApiError,
|
|
43
|
+
from cvprac.cvp_client import CvpClient, url_with_port
|
|
44
|
+
from cvprac.cvp_client_errors import (CvpApiError, CvpRequestError,
|
|
45
|
+
CvpSessionLogOutError)
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
class TestClient(unittest.TestCase):
|
|
@@ -200,6 +203,185 @@ class TestClient(unittest.TestCase):
|
|
|
200
203
|
self.assertEqual(self.clnt.url_prefix, url)
|
|
201
204
|
self.assertEqual(self.clnt.error_msg, error)
|
|
202
205
|
|
|
206
|
+
def test_url_with_port_ipv4_and_ipv6(self):
|
|
207
|
+
"""Test replacing the port in a URL."""
|
|
208
|
+
self.assertEqual(url_with_port('https://1.1.1.1:443', 9443),
|
|
209
|
+
'https://1.1.1.1:9443')
|
|
210
|
+
self.assertEqual(url_with_port('https://[2001:db8::1]:443', 9443),
|
|
211
|
+
'https://[2001:db8::1]:9443')
|
|
212
|
+
|
|
213
|
+
def test_validate_certificate(self):
|
|
214
|
+
"""Test certificate validation request uses mTLS endpoint."""
|
|
215
|
+
self.clnt.session = Mock()
|
|
216
|
+
self.clnt.url_prefix_short = 'https://1.1.1.1:443'
|
|
217
|
+
self.clnt.cert = False
|
|
218
|
+
self.clnt.cert_login_port = 9443
|
|
219
|
+
self.clnt.client_cert = ('client.crt', 'client.key')
|
|
220
|
+
self.clnt.connect_timeout = 5
|
|
221
|
+
response = Mock()
|
|
222
|
+
response.ok = True
|
|
223
|
+
response.text = '{}'
|
|
224
|
+
response.headers = {'Location': '/?cert_valid=true'}
|
|
225
|
+
response.cookies = cookiejar_from_dict({'cert_state_token': 'token'})
|
|
226
|
+
self.clnt.session.get.return_value = response
|
|
227
|
+
self.clnt._validate_client_certificate = Mock()
|
|
228
|
+
|
|
229
|
+
self.clnt._validate_certificate()
|
|
230
|
+
|
|
231
|
+
self.clnt._validate_client_certificate.assert_called_once_with()
|
|
232
|
+
self.clnt.session.get.assert_called_once_with(
|
|
233
|
+
'https://1.1.1.1:9443/aaa/v1/validateCertificate',
|
|
234
|
+
headers=self.clnt.headers, timeout=5, verify=False,
|
|
235
|
+
cert=('client.crt', 'client.key'), allow_redirects=False)
|
|
236
|
+
self.assertEqual(self.clnt.cookies.get('cert_state_token'), 'token')
|
|
237
|
+
|
|
238
|
+
def test_validate_certificate_location_error(self):
|
|
239
|
+
"""Test validate certificate raises backend redirect errors."""
|
|
240
|
+
self.clnt.session = Mock()
|
|
241
|
+
self.clnt.url_prefix_short = 'https://1.1.1.1:443'
|
|
242
|
+
self.clnt.cert = False
|
|
243
|
+
self.clnt.cert_login_port = 9443
|
|
244
|
+
self.clnt.client_cert = ('client.crt', 'client.key')
|
|
245
|
+
self.clnt.connect_timeout = 5
|
|
246
|
+
response = Mock()
|
|
247
|
+
response.ok = True
|
|
248
|
+
response.text = '{}'
|
|
249
|
+
response.headers = {
|
|
250
|
+
'Location': '/cv?cert_error=%7B%22errorCode%22%3A%22212498%22'
|
|
251
|
+
'%2C%22errorMessage%22%3A%22Failed+to+verify+'
|
|
252
|
+
'client+certificate.+Please+ensure+the+issuing+CA+'
|
|
253
|
+
'is+present+in+the+trusted+certificate+store.%22%7D'
|
|
254
|
+
}
|
|
255
|
+
self.clnt.session.get.return_value = response
|
|
256
|
+
self.clnt._validate_client_certificate = Mock()
|
|
257
|
+
|
|
258
|
+
with self.assertRaisesRegex(CvpApiError,
|
|
259
|
+
'Failed to verify client certificate'):
|
|
260
|
+
self.clnt._validate_certificate()
|
|
261
|
+
|
|
262
|
+
def test_validate_certificate_close_browser_error_trimmed(self):
|
|
263
|
+
"""Test close browser guidance is trimmed from cert errors."""
|
|
264
|
+
self.clnt.session = Mock()
|
|
265
|
+
self.clnt.url_prefix_short = 'https://1.1.1.1:443'
|
|
266
|
+
self.clnt.cert = False
|
|
267
|
+
self.clnt.cert_login_port = 9443
|
|
268
|
+
self.clnt.client_cert = ('client.crt', 'client.key')
|
|
269
|
+
self.clnt.connect_timeout = 5
|
|
270
|
+
response = Mock()
|
|
271
|
+
response.ok = True
|
|
272
|
+
response.text = '{}'
|
|
273
|
+
response.headers = {
|
|
274
|
+
'Location': '/cv?cert_error=%7B%22errorCode%22%3A%22212498%22'
|
|
275
|
+
'%2C%22errorMessage%22%3A%22x+y+z.+Please+close+'
|
|
276
|
+
'the+browser+and+try+again.%22%7D'
|
|
277
|
+
}
|
|
278
|
+
self.clnt.session.get.return_value = response
|
|
279
|
+
self.clnt._validate_client_certificate = Mock()
|
|
280
|
+
|
|
281
|
+
with self.assertRaisesRegex(CvpApiError,
|
|
282
|
+
'Validate certificate: .*x y z$'):
|
|
283
|
+
self.clnt._validate_certificate()
|
|
284
|
+
|
|
285
|
+
def test_validate_client_certificate(self):
|
|
286
|
+
"""Test local client certificate validation loads cert chain."""
|
|
287
|
+
self.clnt.client_cert = ('client.crt', 'client.key')
|
|
288
|
+
context = Mock()
|
|
289
|
+
|
|
290
|
+
with patch('cvprac.cvp_client.ssl.SSLContext',
|
|
291
|
+
return_value=context) as mock_context:
|
|
292
|
+
self.clnt._validate_client_certificate()
|
|
293
|
+
|
|
294
|
+
mock_context.assert_called_once_with(ssl.PROTOCOL_TLS_CLIENT)
|
|
295
|
+
context.load_cert_chain.assert_called_once_with(
|
|
296
|
+
certfile='client.crt', keyfile='client.key')
|
|
297
|
+
|
|
298
|
+
def test_validate_client_certificate_single_file(self):
|
|
299
|
+
"""Test local client certificate validation supports one file."""
|
|
300
|
+
self.clnt.client_cert = 'client.pem'
|
|
301
|
+
context = Mock()
|
|
302
|
+
|
|
303
|
+
with patch('cvprac.cvp_client.ssl.SSLContext',
|
|
304
|
+
return_value=context):
|
|
305
|
+
self.clnt._validate_client_certificate()
|
|
306
|
+
|
|
307
|
+
context.load_cert_chain.assert_called_once_with(
|
|
308
|
+
certfile='client.pem', keyfile=None)
|
|
309
|
+
|
|
310
|
+
def test_validate_client_certificate_invalid_file(self):
|
|
311
|
+
"""Test bad local client certificate raises a useful error."""
|
|
312
|
+
self.clnt.client_cert = ('bad.crt', 'bad.key')
|
|
313
|
+
context = Mock()
|
|
314
|
+
context.load_cert_chain.side_effect = ssl.SSLError(
|
|
315
|
+
'PEM lib')
|
|
316
|
+
|
|
317
|
+
with patch('cvprac.cvp_client.ssl.SSLContext',
|
|
318
|
+
return_value=context):
|
|
319
|
+
with self.assertRaisesRegex(CvpRequestError, 'Invalid client certificate or key'):
|
|
320
|
+
self.clnt._validate_client_certificate()
|
|
321
|
+
|
|
322
|
+
def test_validate_client_certificate_bad_tuple(self):
|
|
323
|
+
"""Test malformed client_cert values fail before requests."""
|
|
324
|
+
self.clnt.client_cert = ('client.crt', 'client.key', 'extra')
|
|
325
|
+
|
|
326
|
+
with self.assertRaisesRegex(CvpRequestError,
|
|
327
|
+
'Invalid client certificate/key'):
|
|
328
|
+
self.clnt._validate_client_certificate()
|
|
329
|
+
|
|
330
|
+
def test_validate_certificate_bad_client_cert_skips_request(self):
|
|
331
|
+
"""Test bad local client cert prevents validateCertificate call."""
|
|
332
|
+
self.clnt.session = Mock()
|
|
333
|
+
self.clnt.url_prefix_short = 'https://1.1.1.1:443'
|
|
334
|
+
self.clnt.cert = False
|
|
335
|
+
self.clnt.cert_login_port = 9443
|
|
336
|
+
self.clnt.client_cert = ('bad.crt', 'bad.key')
|
|
337
|
+
self.clnt._validate_client_certificate = Mock(
|
|
338
|
+
side_effect=CvpRequestError(
|
|
339
|
+
'Invalid client certificate/key: PEM lib'))
|
|
340
|
+
|
|
341
|
+
with self.assertRaisesRegex(CvpRequestError,
|
|
342
|
+
'Invalid client certificate/key'):
|
|
343
|
+
self.clnt._validate_certificate()
|
|
344
|
+
self.clnt.session.get.assert_not_called()
|
|
345
|
+
|
|
346
|
+
def test_login_on_prem_with_certificate(self):
|
|
347
|
+
"""Test certificate based login validates cert before auth."""
|
|
348
|
+
self.clnt.session = Mock()
|
|
349
|
+
self.clnt.url_prefix = 'https://1.1.1.1:443/web'
|
|
350
|
+
self.clnt.authdata = {'userId': 'certuser', 'password': 'password'}
|
|
351
|
+
self.clnt.connect_timeout = 10
|
|
352
|
+
self.clnt.cert = False
|
|
353
|
+
self.clnt.cert_login = True
|
|
354
|
+
self.clnt.cert_login_port = 9443
|
|
355
|
+
self.clnt.client_cert = ('client.crt', 'client.key')
|
|
356
|
+
self.clnt.cookies = cookiejar_from_dict({'cert_state_token': 'token'})
|
|
357
|
+
self.clnt._validate_certificate = Mock()
|
|
358
|
+
response = Mock()
|
|
359
|
+
response.ok = True
|
|
360
|
+
response.text = '{"sessionId": "SESSION"}'
|
|
361
|
+
response.json.return_value = {'sessionId': 'SESSION'}
|
|
362
|
+
response.cookies = cookiejar_from_dict({'access_token': 'ACCESS'})
|
|
363
|
+
self.clnt.session.post.return_value = response
|
|
364
|
+
|
|
365
|
+
self.clnt._login_on_prem()
|
|
366
|
+
|
|
367
|
+
self.clnt._validate_certificate.assert_called_once_with()
|
|
368
|
+
self.clnt.session.post.assert_called_once_with(
|
|
369
|
+
'https://1.1.1.1:443/web/login/authenticate.do',
|
|
370
|
+
data=json.dumps({'userId': 'certuser', 'password': 'password'}),
|
|
371
|
+
headers=self.clnt.headers, cookies=self.clnt.cookies, timeout=10,
|
|
372
|
+
verify=False, cert=('client.crt', 'client.key'))
|
|
373
|
+
self.assertEqual(self.clnt.headers['APP_SESSION_ID'], 'SESSION')
|
|
374
|
+
self.assertEqual(self.clnt.cookies.get('access_token'), 'ACCESS')
|
|
375
|
+
|
|
376
|
+
def test_login_on_prem_with_certificate_requires_client_cert(self):
|
|
377
|
+
"""Test certificate login requires a client cert."""
|
|
378
|
+
self.clnt.url_prefix = 'https://1.1.1.1:443/web'
|
|
379
|
+
self.clnt.cert_login = True
|
|
380
|
+
self.clnt.client_cert = None
|
|
381
|
+
|
|
382
|
+
with self.assertRaises(CvpRequestError):
|
|
383
|
+
self.clnt._login_on_prem()
|
|
384
|
+
|
|
203
385
|
def test_make_request_good(self):
|
|
204
386
|
""" Test request does not raise exception and returns json.
|
|
205
387
|
"""
|
cvprac-2.2.0/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
2.2.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py
RENAMED
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_all_devices_legacy.py
RENAMED
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_and_decommission_device.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/assign_configlet_to_device.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/create_configlet_from_file.py
RENAMED
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/get_applied_netelements.py
RENAMED
|
File without changes
|
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/reorder_configlet_on_device.py
RENAMED
|
File without changes
|
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/add_image_to_container.py
RENAMED
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/assign_configlet_to_container.py
RENAMED
|
File without changes
|
|
File without changes
|
{cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/remove_image_from_container.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|