cvprac 2.0.0__tar.gz → 2.2.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 (115) hide show
  1. {cvprac-2.0.0 → cvprac-2.2.0}/PKG-INFO +25 -7
  2. cvprac-2.2.0/VERSION +1 -0
  3. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac/__init__.py +1 -1
  4. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac/cvp_api.py +124 -15
  5. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac.egg-info/PKG-INFO +25 -7
  6. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac.egg-info/SOURCES.txt +2 -0
  7. cvprac-2.2.0/docs/release-notes-2.1.0.rst +10 -0
  8. cvprac-2.2.0/docs/release-notes-2.2.0.rst +10 -0
  9. {cvprac-2.0.0 → cvprac-2.2.0}/test/fixtures/cvp_nodes.yaml +1 -1
  10. {cvprac-2.0.0 → cvprac-2.2.0}/test/system/test_cvp_base.py +66 -9
  11. {cvprac-2.0.0 → cvprac-2.2.0}/test/system/test_cvp_change_control_api.py +123 -12
  12. {cvprac-2.0.0 → cvprac-2.2.0}/test/system/test_cvp_client_api.py +8 -9
  13. {cvprac-2.0.0 → cvprac-2.2.0}/test/unit/test_api.py +156 -0
  14. cvprac-2.0.0/VERSION +0 -1
  15. {cvprac-2.0.0 → cvprac-2.2.0}/.pylintrc +0 -0
  16. {cvprac-2.0.0 → cvprac-2.2.0}/LICENSE +0 -0
  17. {cvprac-2.0.0 → cvprac-2.2.0}/MANIFEST.in +0 -0
  18. {cvprac-2.0.0 → cvprac-2.2.0}/Makefile +0 -0
  19. {cvprac-2.0.0 → cvprac-2.2.0}/README.md +0 -0
  20. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac/cvp_client.py +0 -0
  21. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac/cvp_client_errors.py +0 -0
  22. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac.egg-info/dependency_links.txt +0 -0
  23. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac.egg-info/requires.txt +0 -0
  24. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac.egg-info/top_level.txt +0 -0
  25. {cvprac-2.0.0 → cvprac-2.2.0}/cvprac.spec +0 -0
  26. {cvprac-2.0.0 → cvprac-2.2.0}/dev-requirements.txt +0 -0
  27. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/README.md +0 -0
  28. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab01-cvp-info/get_cvp_info.py +0 -0
  29. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab02-inventory-operations/compliance_check.py +0 -0
  30. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab02-inventory-operations/get_running_configs.py +0 -0
  31. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py +0 -0
  32. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab02-inventory-operations/remove_all_devices_legacy.py +0 -0
  33. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab02-inventory-operations/remove_and_decommission_device.py +0 -0
  34. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab02-inventory-operations/remove_devices_from_container_legacy.py +0 -0
  35. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab02-inventory-operations/remove_devices_legacy.py +0 -0
  36. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/assign_configlet_to_device.py +0 -0
  37. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/backup_configlets.py +0 -0
  38. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/backup_configletsV2.py +0 -0
  39. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/common.cfg +0 -0
  40. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/config_search.py +0 -0
  41. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/configlet_list.txt +0 -0
  42. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/create_configlet.py +0 -0
  43. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/create_configlet_from_file.py +0 -0
  44. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/get_applied_netelements.py +0 -0
  45. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/get_configlets.py +0 -0
  46. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/reorder_configlet_on_device.py +0 -0
  47. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab03-configlet-management/update_configlet.py +0 -0
  48. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab04-container-management/add_image_to_container.py +0 -0
  49. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab04-container-management/assign_configlet_to_container.py +0 -0
  50. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab04-container-management/create_container.py +0 -0
  51. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab04-container-management/remove_image_from_container.py +0 -0
  52. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab04-container-management/rename_container.py +0 -0
  53. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab05-device-management/add_image_to_devices.py +0 -0
  54. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab05-device-management/add_image_wo_tempaction.py +0 -0
  55. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab05-device-management/remove_image_from_device.py +0 -0
  56. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab05-device-management/set_mgmt_ip.py +0 -0
  57. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/atd_e2e_provisioning_workflow.py +0 -0
  58. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/auto_reconcile_on_rc_change.py +0 -0
  59. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/change_control_custom_rapi.py +0 -0
  60. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/change_control_workflow_rapi.py +0 -0
  61. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf1.cfg +0 -0
  62. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf2.cfg +0 -0
  63. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf3.cfg +0 -0
  64. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/configlets/AVD_leaf4.cfg +0 -0
  65. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/configlets/AVD_spine1.cfg +0 -0
  66. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/configlets/AVD_spine2.cfg +0 -0
  67. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/gen_builder.py +0 -0
  68. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/mlag_issu.py +0 -0
  69. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/move_device.py +0 -0
  70. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab06-provisioning/vc_task_retrigger.py +0 -0
  71. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/aaa_users.csv +0 -0
  72. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/add_new_user_cvaas.py +0 -0
  73. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/add_new_user_onprem.py +0 -0
  74. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/add_users_from_csv_cvaas.py +0 -0
  75. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/create_svc_account.py +0 -0
  76. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/create_svc_account_token.py +0 -0
  77. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/create_terminattr_tokens.py +0 -0
  78. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/cvaas.tok +0 -0
  79. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/delete_all_expired_svc_account_tokens.py +0 -0
  80. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/delete_svc_account.py +0 -0
  81. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/delete_svc_account_created_by_user.py +0 -0
  82. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/get_user_info.py +0 -0
  83. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab07-aaa/svc_account_misc.py +0 -0
  84. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab08-resource-apis/resource_cvprac.py +0 -0
  85. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/lab08-resource-apis/topology_tag_assignment.py +0 -0
  86. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/static/serviceaccount1.png +0 -0
  87. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/static/serviceaccount2.png +0 -0
  88. {cvprac-2.0.0 → cvprac-2.2.0}/docs/labs/static/serviceaccount3.png +0 -0
  89. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-0.7.0.rst +0 -0
  90. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-0.8.0.rst +0 -0
  91. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-0.9.0.rst +0 -0
  92. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.0.0.rst +0 -0
  93. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.0.1.rst +0 -0
  94. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.0.2.rst +0 -0
  95. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.0.3.rst +0 -0
  96. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.0.4.rst +0 -0
  97. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.0.5.rst +0 -0
  98. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.0.6.rst +0 -0
  99. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.0.7.rst +0 -0
  100. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.2.0.rst +0 -0
  101. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.2.2.rst +0 -0
  102. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.3.0.rst +0 -0
  103. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.3.1.rst +0 -0
  104. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.3.2.rst +0 -0
  105. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.4.0.rst +0 -0
  106. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.4.1.rst +0 -0
  107. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-1.4.2.rst +0 -0
  108. {cvprac-2.0.0 → cvprac-2.2.0}/docs/release-notes-2.0.0.rst +0 -0
  109. {cvprac-2.0.0 → cvprac-2.2.0}/requirements.txt +0 -0
  110. {cvprac-2.0.0 → cvprac-2.2.0}/setup.cfg +0 -0
  111. {cvprac-2.0.0 → cvprac-2.2.0}/setup.py +0 -0
  112. {cvprac-2.0.0 → cvprac-2.2.0}/test/fixtures/image-file.swix +0 -0
  113. {cvprac-2.0.0 → cvprac-2.2.0}/test/lib/systestlib.py +0 -0
  114. {cvprac-2.0.0 → cvprac-2.2.0}/test/system/test_cvp_client.py +0 -0
  115. {cvprac-2.0.0 → cvprac-2.2.0}/test/unit/test_client.py +0 -0
@@ -1,14 +1,13 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: cvprac
3
- Version: 2.0.0
3
+ Version: 2.2.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
7
  Author: Arista Networks, Inc.
7
8
  Author-email: eosplus-dev@arista.com
8
9
  License: BSD-3
9
- Download-URL: https://github.com/aristanetworks/cvprac/tarball/2.0.0
10
10
  Keywords: networking CloudVision development rest api
11
- Platform: UNKNOWN
12
11
  Classifier: Development Status :: 4 - Beta
13
12
  Classifier: Intended Audience :: Developers
14
13
  Classifier: Intended Audience :: System Administrators
@@ -24,8 +23,29 @@ Classifier: Programming Language :: Python :: 3.10
24
23
  Classifier: Programming Language :: Python :: 3.11
25
24
  Classifier: Programming Language :: Python :: 3.12
26
25
  Description-Content-Type: text/markdown
27
- Provides-Extra: dev
28
26
  License-File: LICENSE
27
+ Requires-Dist: requests[socks]>=2.27.0
28
+ Requires-Dist: packaging>=23.2
29
+ Provides-Extra: dev
30
+ Requires-Dist: check-manifest; extra == "dev"
31
+ Requires-Dist: pep8; extra == "dev"
32
+ Requires-Dist: pyflakes; extra == "dev"
33
+ Requires-Dist: pylint; extra == "dev"
34
+ Requires-Dist: coverage; extra == "dev"
35
+ Requires-Dist: pyyaml; extra == "dev"
36
+ Dynamic: author
37
+ Dynamic: author-email
38
+ Dynamic: classifier
39
+ Dynamic: description
40
+ Dynamic: description-content-type
41
+ Dynamic: download-url
42
+ Dynamic: home-page
43
+ Dynamic: keywords
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: provides-extra
47
+ Dynamic: requires-dist
48
+ Dynamic: summary
29
49
 
30
50
  # Arista Cloudvision® Portal RESTful API Client
31
51
 
@@ -478,5 +498,3 @@ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
478
498
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
479
499
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
480
500
  THE POSSIBILITY OF SUCH DAMAGE.
481
-
482
-
cvprac-2.2.0/VERSION ADDED
@@ -0,0 +1 @@
1
+ 2.2.0
@@ -32,5 +32,5 @@
32
32
  ''' RESTful API Client class for Cloudvision(R) Portal
33
33
  '''
34
34
 
35
- __version__ = '2.0.0'
35
+ __version__ = '2.2.0'
36
36
  __author__ = 'Arista Networks, Inc.'
@@ -471,6 +471,22 @@ class CvpApi():
471
471
  return self.clnt.post('/task/cancelTask.do', data=data,
472
472
  timeout=self.request_timeout)
473
473
 
474
+ def get_config_diff_for_task(self, task_id):
475
+ ''' Get the config diff and validation errors for a specific task.
476
+ Uses the compliance check API to compare designed vs running config.
477
+
478
+ Args:
479
+ task_id (str): The task ID to check.
480
+
481
+ Returns:
482
+ response (list): A list of dicts containing sources, diff, and
483
+ error entries. Error entries have error_code, error_msg,
484
+ line_num, configlet_name, and config_line_num fields.
485
+ '''
486
+ diff_url = '/api/v3/services/compliancecheck.Compliance/GetConfigDiffForTask'
487
+ return self.clnt.post(diff_url, data={"task_id": str(task_id)},
488
+ timeout=self.request_timeout)
489
+
474
490
  def get_configlets(self, start=0, end=0):
475
491
  ''' Returns a list of all defined configlets.
476
492
 
@@ -685,6 +701,7 @@ class CvpApi():
685
701
  return data
686
702
 
687
703
  # pylint: disable=too-many-locals
704
+ # pylint: disable=too-many-branches
688
705
  def add_devices_to_inventory(self, device_list, wait=False, move_to_container=True):
689
706
  ''' Add list of devices to inventory and optionally move them to specified parent container.
690
707
 
@@ -714,6 +731,10 @@ class CvpApi():
714
731
  parent_key: 'container-id-1234'
715
732
  }
716
733
  ]
734
+ Returns: Dict of devices that failed to onboard in below format. None if all devices
735
+ onboarded successfully.
736
+
737
+ {'192.168.0.11': "ERROR"}
717
738
  '''
718
739
 
719
740
  self.log.debug('add_devices_to_inventory: called')
@@ -735,17 +756,27 @@ class CvpApi():
735
756
  self.clnt.post('/inventory/add/addToInventory.do?'
736
757
  'startIndex=0&endIndex=0', data=data,
737
758
  timeout=self.request_timeout)
738
- else:
739
- self.log.debug('v2 Inventory API Call')
740
-
741
- # Create a list of device IPs
742
- device_ips = [dev['device_ip'] for dev in device_list]
743
-
744
- # First add the devices to the inventory in a single call
745
- data = {'hosts': device_ips}
746
- self.clnt.post('/inventory/devices', data=data,
747
- timeout=self.request_timeout)
759
+ return None
760
+ self.log.debug('v2 Inventory API Call')
748
761
 
762
+ # Create a list of device IPs
763
+ device_ips = [dev['device_ip'] for dev in device_list]
764
+
765
+ # First add the devices to the inventory in a single call
766
+ data = {'hosts': device_ips}
767
+ failed_onboard = None
768
+ resp = self.clnt.post('/inventory/devices', data=data, timeout=self.request_timeout)
769
+ if resp and 'failed' in resp['data']:
770
+ failed_onboard = resp['data']['failed']
771
+ for failed_dev_ip in failed_onboard:
772
+ self.log.warning(
773
+ 'Found error onboarding device %s - %s',
774
+ failed_dev_ip,
775
+ failed_onboard[failed_dev_ip]
776
+ )
777
+ device_ips.remove(failed_dev_ip)
778
+
779
+ if device_ips:
749
780
  # Get the inventory list
750
781
  inv = self.get_inventory()
751
782
 
@@ -771,11 +802,14 @@ class CvpApi():
771
802
  for device in device_list:
772
803
  devs = [dev for dev in inv if 'ipAddress' in dev and
773
804
  device['device_ip'] in dev['ipAddress']]
774
- dev = devs[0]
775
- container = {'key': device['parent_key'],
776
- 'name': device['parent_name']}
777
- self.move_device_to_container('add_devices_to_inventory API v2',
778
- dev, container, False)
805
+ # Only attempt to move device to another container if it is in inventory
806
+ if devs:
807
+ dev = devs[0]
808
+ container = {'key': device['parent_key'],
809
+ 'name': device['parent_name']}
810
+ self.move_device_to_container('add_devices_to_inventory API v2',
811
+ dev, container, False)
812
+ return failed_onboard
779
813
 
780
814
  def add_device_to_inventory(self, device_ip, parent_name,
781
815
  parent_key, wait=False, move_to_container=True):
@@ -3040,6 +3074,24 @@ class CvpApi():
3040
3074
  return self.clnt.get(cc_url, timeout=self.request_timeout)
3041
3075
  return None
3042
3076
 
3077
+ def change_control_get_task_errors(self, cc_id):
3078
+ ''' Get task configuration errors for a change control.
3079
+ Supported versions: CVP 2021.2.0 or newer and CVaaS.
3080
+
3081
+ Args:
3082
+ cc_id (str): The ID of the change control.
3083
+
3084
+ Returns:
3085
+ errors (list): A list of (task_id, error_dict) tuples for any tasks
3086
+ with DEVICEERROR entries. Empty list if no errors found.
3087
+ Returns None if the change control does not exist or the API
3088
+ is unsupported.
3089
+ '''
3090
+ cc_status = self.change_control_get_one(cc_id)
3091
+ if cc_status is None:
3092
+ return None
3093
+ return self._parse_task_errors_from_cc_data(cc_status)
3094
+
3043
3095
  def change_control_approval_get_one(self, cc_id, cc_time=None):
3044
3096
  ''' Get the state of a specific Change Control's approve config using Resource APIs.
3045
3097
  Supported versions: CVP 2021.2.0 or newer and CVaaS.
@@ -3093,6 +3145,10 @@ class CvpApi():
3093
3145
  ''' Approve/Unapprove a change control using Resource APIs.
3094
3146
  Supported versions: CVP 2021.2.0 or newer and CVaaS.
3095
3147
 
3148
+ When approving (approve=True), validates that no tasks in the
3149
+ change control have configuration errors. Raises CvpApiError
3150
+ if any task has DEVICEERROR entries from the compliance check.
3151
+
3096
3152
  Args:
3097
3153
  cc_id (str): The ID of the change control.
3098
3154
  notes (str): An optional approval note.
@@ -3111,6 +3167,19 @@ class CvpApi():
3111
3167
  else:
3112
3168
  self.log.error('The version timestamp was not found in the CC status.')
3113
3169
  return None
3170
+
3171
+ if approve:
3172
+ task_errors = self._parse_task_errors_from_cc_data(cc_status)
3173
+ if task_errors:
3174
+ error_details = "; ".join(
3175
+ f"Task {tid}: {err['error_msg']}"
3176
+ for tid, err in task_errors
3177
+ )
3178
+ raise CvpApiError(
3179
+ f"Cannot approve change control {cc_id}. "
3180
+ f"Tasks have configuration errors: {error_details}"
3181
+ )
3182
+
3114
3183
  payload = {
3115
3184
  "key": {
3116
3185
  "id": cc_id
@@ -3123,6 +3192,46 @@ class CvpApi():
3123
3192
  }
3124
3193
  return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout)
3125
3194
 
3195
+ def _parse_task_errors_from_cc_data(self, cc_data):
3196
+ ''' Extract task IDs from a change control data and check each for config errors.
3197
+ Make an API call on each task and parse the config diff for errors.
3198
+
3199
+ Args:
3200
+ cc_data (dict): The change control dict from change_control_get_one.
3201
+
3202
+ Returns:
3203
+ errors (list): A list of (task_id, error_dict) tuples for any tasks
3204
+ with DEVICEERROR entries. Empty list if no errors found.
3205
+ error_dict example: {
3206
+ "error_code": "DEVICEERROR",
3207
+ "error_msg": "> typocommand test % Invalid input (at token 0: 'typocommand')",
3208
+ "line_num": 50,
3209
+ "configlet_name": "device-name",
3210
+ "config_line_num": 54
3211
+ }
3212
+ '''
3213
+ errors = []
3214
+ stages = (cc_data.get('value', {}).get('change', {})
3215
+ .get('stages', {}).get('values', {}))
3216
+ for stage in stages.values():
3217
+ action = stage.get('action', {})
3218
+ if action.get('name') == 'task':
3219
+ task_id = (action.get('args', {}).get('values', {})
3220
+ .get('TaskID'))
3221
+ if task_id:
3222
+ try:
3223
+ resp = self.get_config_diff_for_task(task_id)
3224
+ except Exception as error:
3225
+ self.log.error(
3226
+ f"Failed to get config diff for task {task_id}: {error}")
3227
+ continue
3228
+ if isinstance(resp, list):
3229
+ for entry in resp:
3230
+ err = entry.get('error') if isinstance(entry, dict) else None
3231
+ if err and err.get('error_code') == 'DEVICEERROR':
3232
+ errors.append((task_id, err))
3233
+ return errors
3234
+
3126
3235
  def change_control_delete(self, cc_id):
3127
3236
  ''' Delete a pending Change Control using Resource APIs.
3128
3237
  Supported versions: CVP 2021.2.0 or newer and CVaaS.
@@ -1,14 +1,13 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: cvprac
3
- Version: 2.0.0
3
+ Version: 2.2.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
7
  Author: Arista Networks, Inc.
7
8
  Author-email: eosplus-dev@arista.com
8
9
  License: BSD-3
9
- Download-URL: https://github.com/aristanetworks/cvprac/tarball/2.0.0
10
10
  Keywords: networking CloudVision development rest api
11
- Platform: UNKNOWN
12
11
  Classifier: Development Status :: 4 - Beta
13
12
  Classifier: Intended Audience :: Developers
14
13
  Classifier: Intended Audience :: System Administrators
@@ -24,8 +23,29 @@ Classifier: Programming Language :: Python :: 3.10
24
23
  Classifier: Programming Language :: Python :: 3.11
25
24
  Classifier: Programming Language :: Python :: 3.12
26
25
  Description-Content-Type: text/markdown
27
- Provides-Extra: dev
28
26
  License-File: LICENSE
27
+ Requires-Dist: requests[socks]>=2.27.0
28
+ Requires-Dist: packaging>=23.2
29
+ Provides-Extra: dev
30
+ Requires-Dist: check-manifest; extra == "dev"
31
+ Requires-Dist: pep8; extra == "dev"
32
+ Requires-Dist: pyflakes; extra == "dev"
33
+ Requires-Dist: pylint; extra == "dev"
34
+ Requires-Dist: coverage; extra == "dev"
35
+ Requires-Dist: pyyaml; extra == "dev"
36
+ Dynamic: author
37
+ Dynamic: author-email
38
+ Dynamic: classifier
39
+ Dynamic: description
40
+ Dynamic: description-content-type
41
+ Dynamic: download-url
42
+ Dynamic: home-page
43
+ Dynamic: keywords
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: provides-extra
47
+ Dynamic: requires-dist
48
+ Dynamic: summary
29
49
 
30
50
  # Arista Cloudvision® Portal RESTful API Client
31
51
 
@@ -478,5 +498,3 @@ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
478
498
  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
479
499
  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
480
500
  THE POSSIBILITY OF SUCH DAMAGE.
481
-
482
-
@@ -37,6 +37,8 @@ docs/release-notes-1.4.0.rst
37
37
  docs/release-notes-1.4.1.rst
38
38
  docs/release-notes-1.4.2.rst
39
39
  docs/release-notes-2.0.0.rst
40
+ docs/release-notes-2.1.0.rst
41
+ docs/release-notes-2.2.0.rst
40
42
  docs/labs/README.md
41
43
  docs/labs/lab01-cvp-info/get_cvp_info.py
42
44
  docs/labs/lab02-inventory-operations/compliance_check.py
@@ -0,0 +1,10 @@
1
+ ######
2
+ v2.1.0
3
+ ######
4
+
5
+ 2026-5-26
6
+
7
+ Enhancements
8
+ ^^^^^^^^^^^^
9
+
10
+ * Handle devices that fail to onboard in add_devices_to_inventory. (`311 <https://github.com/aristanetworks/cvprac/pull/311>`_) [`mharista <https://github.com/mharista>`_]
@@ -0,0 +1,10 @@
1
+ ######
2
+ v2.2.0
3
+ ######
4
+
5
+ 2026-6-24
6
+
7
+ Enhancements
8
+ ^^^^^^^^^^^^
9
+
10
+ * Add check for Validation Errors on Tasks before CC Approval. (`317 <https://github.com/aristanetworks/cvprac/pull/317>`_) [`chetryan <https://github.com/chetryan>`_]
@@ -1,4 +1,4 @@
1
- - node: 10.255.0.159
1
+ - node: 10.18.134.138
2
2
  username: cvpadmin
3
3
  password: cvp123!
4
4
  device: veos1
@@ -13,6 +13,7 @@ import urllib3
13
13
  sys.path.append(os.path.join(os.path.dirname(__file__), '../lib'))
14
14
  from systestlib import DutSystemTest
15
15
  from cvprac.cvp_client import CvpClient
16
+ from cvprac.cvp_client_errors import CvpApiError
16
17
 
17
18
 
18
19
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
@@ -101,7 +102,6 @@ class TestCvpClientBase(DutSystemTest):
101
102
  '''
102
103
  self.task_id = None
103
104
  self.cc_id = str(uuid.uuid4())
104
- # self.cc_name = 'test_api_%d %s' % time.time()
105
105
  self.cc_name = f'test_api_{time.time()}'
106
106
 
107
107
  def tearDown(self):
@@ -113,11 +113,25 @@ class TestCvpClientBase(DutSystemTest):
113
113
  if chg_ctrl_get_one:
114
114
  # Delete CC
115
115
  self.delete_change_control(self.cc_id)
116
- # Cancel task
117
- if self.task_id:
118
- task = self.api.get_task_by_id(self.task_id)
119
- if task and task['currentTaskName'] != "Cancelled":
120
- self.cancel_task(self.task_id)
116
+ self.cc_id = None
117
+ # Cancel task
118
+ if self.task_id:
119
+ # Do not attempt to cancel tasks with taskStatus
120
+ # in following list.
121
+ ignore_task_status = ["COMPLETED"]
122
+ # Do not attempt to cancel tasks with currentTaskName
123
+ # in following list.
124
+ ignore_current_task_name = ["Cancelled"]
125
+ task = self.api.get_task_by_id(self.task_id)
126
+ if task:
127
+ if task["currentTaskName"] not in ignore_current_task_name:
128
+ if task["taskStatus"] not in ignore_task_status:
129
+ try:
130
+ self.cancel_task(self.task_id)
131
+ except CvpApiError as err:
132
+ print(f"Error while trying to cancel task {self.task_id} - {err}")
133
+ print(f"Task info - {task}")
134
+ self.task_id = None
121
135
 
122
136
  def _get_next_task_id(self):
123
137
  ''' Return the next task id.
@@ -146,10 +160,10 @@ class TestCvpClientBase(DutSystemTest):
146
160
  configlet = None
147
161
  for conf in self.dev_configlets:
148
162
  if conf['netElementCount'] == 1:
149
- configlet = conf
163
+ configlet = conf.copy()
150
164
  break
151
165
  if configlet is None:
152
- configlet = self.dev_configlets[0]
166
+ configlet = self.dev_configlets[0].copy()
153
167
 
154
168
  config = configlet['config']
155
169
  org_config = config
@@ -184,7 +198,50 @@ class TestCvpClientBase(DutSystemTest):
184
198
  err_msg = f'Timeout waiting for task id {task_id} to be created'
185
199
  self.assertGreater(cnt, 0, msg=err_msg)
186
200
  self.task_id = task_id
187
- return task_id, org_config
201
+ return task_id, org_config, configlet
202
+
203
+ def _create_task_with_invalid_config(self):
204
+ ''' Create a task by adding an invalid EOS command to a configlet.
205
+ The invalid command will cause a DEVICEERROR in the config diff
206
+ compliance check, which is used to test the task error validation
207
+ in change_control_approve.
208
+
209
+ Returns:
210
+ (task_id, org_config, configlet)
211
+ task_id (str): Task ID
212
+ org_config (str): Original configlet contents for cleanup
213
+ configlet (dict): The configlet dict for cleanup
214
+ '''
215
+ task_id = self._get_next_task_id()
216
+ configlet = None
217
+ for conf in self.dev_configlets:
218
+ if conf['netElementCount'] == 1:
219
+ configlet = conf.copy()
220
+ break
221
+ if configlet is None:
222
+ configlet = self.dev_configlets[0].copy()
223
+
224
+ org_config = configlet['config']
225
+ config = org_config + '\ntypocommand test\n'
226
+ configlet['config'] = config
227
+
228
+ self.api.update_configlet(config, configlet['key'], configlet['name'])
229
+
230
+ cnt = 30
231
+ if self.clnt.apiversion is None:
232
+ self.api.get_cvp_info()
233
+ if self.clnt.apiversion >= 2.0:
234
+ cnt += 30
235
+ while cnt > 0:
236
+ time.sleep(1)
237
+ result = self.api.get_task_by_id(task_id)
238
+ if result is not None:
239
+ break
240
+ cnt -= 1
241
+ err_msg = f'Timeout waiting for task id {task_id} to be created'
242
+ self.assertGreater(cnt, 0, msg=err_msg)
243
+ self.task_id = task_id
244
+ return task_id, org_config, configlet
188
245
 
189
246
  def delete_change_control(self, cc_id):
190
247
  """ Delete change control