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.
Files changed (117) hide show
  1. {cvprac-2.2.0 → cvprac-2.3.0}/PKG-INFO +15 -24
  2. {cvprac-2.2.0 → cvprac-2.3.0}/README.md +13 -22
  3. cvprac-2.3.0/VERSION +1 -0
  4. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac/__init__.py +1 -1
  5. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac/cvp_api.py +28 -0
  6. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac/cvp_client.py +132 -4
  7. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/PKG-INFO +15 -24
  8. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/SOURCES.txt +2 -0
  9. cvprac-2.3.0/docs/release-notes-2.3.0.rst +17 -0
  10. {cvprac-2.2.0 → cvprac-2.3.0}/test/fixtures/cvp_nodes.yaml +1 -1
  11. cvprac-2.3.0/test/fixtures/image-file.swix +0 -0
  12. {cvprac-2.2.0 → cvprac-2.3.0}/test/system/test_cvp_client_api.py +9 -2
  13. {cvprac-2.2.0 → cvprac-2.3.0}/test/unit/test_client.py +185 -3
  14. cvprac-2.2.0/VERSION +0 -1
  15. {cvprac-2.2.0 → cvprac-2.3.0}/.pylintrc +0 -0
  16. {cvprac-2.2.0 → cvprac-2.3.0}/LICENSE +0 -0
  17. {cvprac-2.2.0 → cvprac-2.3.0}/MANIFEST.in +0 -0
  18. {cvprac-2.2.0 → cvprac-2.3.0}/Makefile +0 -0
  19. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac/cvp_client_errors.py +0 -0
  20. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/dependency_links.txt +0 -0
  21. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/requires.txt +0 -0
  22. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.egg-info/top_level.txt +0 -0
  23. {cvprac-2.2.0 → cvprac-2.3.0}/cvprac.spec +0 -0
  24. {cvprac-2.2.0 → cvprac-2.3.0}/dev-requirements.txt +0 -0
  25. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/README.md +0 -0
  26. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab01-cvp-info/get_cvp_info.py +0 -0
  27. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/compliance_check.py +0 -0
  28. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/get_running_configs.py +0 -0
  29. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py +0 -0
  30. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_all_devices_legacy.py +0 -0
  31. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_and_decommission_device.py +0 -0
  32. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_devices_from_container_legacy.py +0 -0
  33. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab02-inventory-operations/remove_devices_legacy.py +0 -0
  34. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/assign_configlet_to_device.py +0 -0
  35. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/backup_configlets.py +0 -0
  36. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/backup_configletsV2.py +0 -0
  37. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/common.cfg +0 -0
  38. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/config_search.py +0 -0
  39. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/configlet_list.txt +0 -0
  40. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/create_configlet.py +0 -0
  41. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/create_configlet_from_file.py +0 -0
  42. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/get_applied_netelements.py +0 -0
  43. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/get_configlets.py +0 -0
  44. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/reorder_configlet_on_device.py +0 -0
  45. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab03-configlet-management/update_configlet.py +0 -0
  46. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/add_image_to_container.py +0 -0
  47. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/assign_configlet_to_container.py +0 -0
  48. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/create_container.py +0 -0
  49. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/remove_image_from_container.py +0 -0
  50. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab04-container-management/rename_container.py +0 -0
  51. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab05-device-management/add_image_to_devices.py +0 -0
  52. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab05-device-management/add_image_wo_tempaction.py +0 -0
  53. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab05-device-management/remove_image_from_device.py +0 -0
  54. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab05-device-management/set_mgmt_ip.py +0 -0
  55. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/atd_e2e_provisioning_workflow.py +0 -0
  56. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/auto_reconcile_on_rc_change.py +0 -0
  57. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/change_control_custom_rapi.py +0 -0
  58. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/change_control_workflow_rapi.py +0 -0
  59. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf1.cfg +0 -0
  60. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf2.cfg +0 -0
  61. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf3.cfg +0 -0
  62. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf4.cfg +0 -0
  63. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_spine1.cfg +0 -0
  64. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/configlets/AVD_spine2.cfg +0 -0
  65. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/gen_builder.py +0 -0
  66. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/mlag_issu.py +0 -0
  67. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/move_device.py +0 -0
  68. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab06-provisioning/vc_task_retrigger.py +0 -0
  69. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/aaa_users.csv +0 -0
  70. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/add_new_user_cvaas.py +0 -0
  71. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/add_new_user_onprem.py +0 -0
  72. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/add_users_from_csv_cvaas.py +0 -0
  73. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/create_svc_account.py +0 -0
  74. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/create_svc_account_token.py +0 -0
  75. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/create_terminattr_tokens.py +0 -0
  76. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/cvaas.tok +0 -0
  77. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/delete_all_expired_svc_account_tokens.py +0 -0
  78. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/delete_svc_account.py +0 -0
  79. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/delete_svc_account_created_by_user.py +0 -0
  80. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/get_user_info.py +0 -0
  81. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab07-aaa/svc_account_misc.py +0 -0
  82. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab08-resource-apis/resource_cvprac.py +0 -0
  83. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/lab08-resource-apis/topology_tag_assignment.py +0 -0
  84. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/static/serviceaccount1.png +0 -0
  85. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/static/serviceaccount2.png +0 -0
  86. {cvprac-2.2.0 → cvprac-2.3.0}/docs/labs/static/serviceaccount3.png +0 -0
  87. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-0.7.0.rst +0 -0
  88. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-0.8.0.rst +0 -0
  89. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-0.9.0.rst +0 -0
  90. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.0.rst +0 -0
  91. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.1.rst +0 -0
  92. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.2.rst +0 -0
  93. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.3.rst +0 -0
  94. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.4.rst +0 -0
  95. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.5.rst +0 -0
  96. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.6.rst +0 -0
  97. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.0.7.rst +0 -0
  98. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.2.0.rst +0 -0
  99. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.2.2.rst +0 -0
  100. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.3.0.rst +0 -0
  101. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.3.1.rst +0 -0
  102. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.3.2.rst +0 -0
  103. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.4.0.rst +0 -0
  104. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.4.1.rst +0 -0
  105. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-1.4.2.rst +0 -0
  106. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-2.0.0.rst +0 -0
  107. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-2.1.0.rst +0 -0
  108. {cvprac-2.2.0 → cvprac-2.3.0}/docs/release-notes-2.2.0.rst +0 -0
  109. {cvprac-2.2.0 → cvprac-2.3.0}/requirements.txt +0 -0
  110. {cvprac-2.2.0 → cvprac-2.3.0}/setup.cfg +0 -0
  111. {cvprac-2.2.0 → cvprac-2.3.0}/setup.py +0 -0
  112. /cvprac-2.2.0/test/fixtures/image-file.swix → /cvprac-2.3.0/test/fixtures/image-file-1787075019.168597.swix +0 -0
  113. {cvprac-2.2.0 → cvprac-2.3.0}/test/lib/systestlib.py +0 -0
  114. {cvprac-2.2.0 → cvprac-2.3.0}/test/system/test_cvp_base.py +0 -0
  115. {cvprac-2.2.0 → cvprac-2.3.0}/test/system/test_cvp_change_control_api.py +0 -0
  116. {cvprac-2.2.0 → cvprac-2.3.0}/test/system/test_cvp_client.py +0 -0
  117. {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.2.0
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.2.0
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 system tests. To run the system tests, you
340
- will need to update the `cvp_nodes.yaml` file found in test/fixtures.
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 system tests. To run the system tests, you
291
- will need to update the `cvp_nodes.yaml` file found in test/fixtures.
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
@@ -32,5 +32,5 @@
32
32
  ''' RESTful API Client class for Cloudvision(R) Portal
33
33
  '''
34
34
 
35
- __version__ = '2.2.0'
35
+ __version__ = '2.3.0'
36
36
  __author__ = 'Arista Networks, Inc.'
@@ -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 = response.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.2.0
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.2.0
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 system tests. To run the system tests, you
340
- will need to update the `cvp_nodes.yaml` file found in test/fixtures.
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>`_]
@@ -1,4 +1,4 @@
1
- - node: 10.18.134.138
1
+ - node: 10.18.140.153
2
2
  username: cvpadmin
3
3
  password: cvp123!
4
4
  device: veos1
@@ -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 a container.
41
- 3) Container or device has at least one configlet applied.
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, CvpSessionLogOutError
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