From 5be30d97549114549cae8c4898813fb1d5573b78 Mon Sep 17 00:00:00 2001 From: MattH Date: Thu, 13 Apr 2023 19:05:16 -0400 Subject: [PATCH 01/11] Bump: versions back to develop post Release v1.3.1 --- VERSION | 2 +- cvprac/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 3a3cd8c..6563189 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.3.1 +develop diff --git a/cvprac/__init__.py b/cvprac/__init__.py index 33ab12d..1a0108c 100644 --- a/cvprac/__init__.py +++ b/cvprac/__init__.py @@ -32,5 +32,5 @@ ''' RESTful API Client class for Cloudvision(R) Portal ''' -__version__ = '1.3.1' +__version__ = 'develop' __author__ = 'Arista Networks, Inc.' From 0d2c8a57f69a3a09c278859873dbc73bccfc59b5 Mon Sep 17 00:00:00 2001 From: mharista Date: Mon, 8 May 2023 10:51:00 -0400 Subject: [PATCH 02/11] Test: Fix test formats for cvprac pipeline environment and until better version handling is implemented (#252) --- test/system/test_cvp_change_control_api.py | 38 ++++++------ test/system/test_cvp_client_api.py | 71 +++++++--------------- 2 files changed, 44 insertions(+), 65 deletions(-) diff --git a/test/system/test_cvp_change_control_api.py b/test/system/test_cvp_change_control_api.py index 1a9af6d..8c75496 100644 --- a/test/system/test_cvp_change_control_api.py +++ b/test/system/test_cvp_change_control_api.py @@ -412,24 +412,28 @@ def test_api_change_control_start_invalid_tasks(self): if self.get_version(): pprint('STARTING CHANGE CONTROL FOR INVALID TASKS...') # Start the change control - dut = self.duts[0] - node = dut['node'] + ":443" + # dut = self.duts[0] + # node = dut['node'] + ":443" # CVP 2022.1.0 format The forward slashes in the error string are likely a bug - pprint('SETTING DEFAULT ERROR MESSAGE FORMAT FOR CVP 2022.1.0') - err_msg = 'POST: https://' + node + '/api/resources/changecontrol/v1/' \ - 'ChangeControlConfig : Request Error:' \ - ' Not Found - {"code":5, "message":"change' \ - ' control with ID' \ - ' \\\\"InvalidCVPRACSystestCCID\\\\"' \ - ' does not exist"}' - if self.clnt.apiversion < 8.0: - # CVP 2021.X.X format - pprint('USING ERROR MESSAGE FORMAT FOR CVP 2021.X.X') - err_msg = "POST: https://" + node + "/api/resources/changecontrol/v1/" \ - "ChangeControlConfig : Request Error: " \ - "Bad Request -" \ - " {\"code\":9,[ ]?\"message\":\"not approved\"}" - with self.assertRaisesRegex(CvpRequestError, err_msg): + # This format fluctuates between CVP 2022.X.X versions so for now we will remove + # the matching of the exact error message format until better version checking + # granularity is implemented. + # pprint('SETTING DEFAULT ERROR MESSAGE FORMAT FOR CVP 2022.1.0') + # err_msg = 'POST: https://' + node + '/api/resources/changecontrol/v1/' \ + # 'ChangeControlConfig : Request Error:' \ + # ' Not Found - {"code":5, "message":"change' \ + # ' control with ID' \ + # ' \\\\"InvalidCVPRACSystestCCID\\\\"' \ + # ' does not exist"}' + # if self.clnt.apiversion < 8.0: + # # CVP 2021.X.X format + # pprint('USING ERROR MESSAGE FORMAT FOR CVP 2021.X.X') + # err_msg = "POST: https://" + node + "/api/resources/changecontrol/v1/" \ + # "ChangeControlConfig : Request Error: " \ + # "Bad Request -" \ + # " {\"code\":9,[ ]?\"message\":\"not approved\"}" + # with self.assertRaisesRegex(CvpRequestError, err_msg): + with self.assertRaises(CvpRequestError): self.start_change_control(CHANGE_CONTROL_ID_INVALID) def test_api_change_control_delete_invalid_cc(self): diff --git a/test/system/test_cvp_client_api.py b/test/system/test_cvp_client_api.py index ac022af..98ea429 100644 --- a/test/system/test_cvp_client_api.py +++ b/test/system/test_cvp_client_api.py @@ -808,53 +808,29 @@ def test_api_get_configlets_and_mappers(self): def test_api_get_configlet_builder(self): ''' Verify get_configlet_builder ''' - try: - # Configlet Builder for pre 2019.x - cfglt = self.api.get_configlet_by_name('SYS_TelemetryBuilderV2') - except CvpApiError as e: - if 'Entity does not exist' in e.msg: - # Configlet Builder for 2019.x - try: - cfglt = self.api.get_configlet_by_name( - 'SYS_TelemetryBuilderV3') - except CvpApiError as e: - if 'Entity does not exist' in e.msg: - # Configlet Builder for 2021.x - 2022.1.1 - try: - cfglt = self.api.get_configlet_by_name( - 'SYS_TelemetryBuilderV4') - except CvpApiError as e: - if 'Entity does not exist' in e.msg: - # Configlet Builder for 2022.2.0 - 2022.3.X - try: - cfglt = self.api.get_configlet_by_name( - 'SYS_TelemetryBuilderV5') - except CvpApiError as e: - if 'Entity does not exist' in e.msg: - # Configlet Builder for 2022.3.X+ - cfglt = self.api.get_configlet_by_name( - 'SYS_TelemetryBuilderV6') - else: - raise - else: - raise - result = self.api.get_configlet_builder(cfglt['key']) + cfglt_bldr_key = None + all_configlets = self.api.get_configlets() + if 'data' in all_configlets: + for cfglt in all_configlets['data']: + if 'name' in cfglt and 'SYS_TelemetryBuilderV' in cfglt['name']: + cfglt_bldr_key = cfglt['key'] + break + if cfglt_bldr_key: + result = self.api.get_configlet_builder(cfglt_bldr_key) - # Verify the following keys and types are - # returned by the request - exp_data = { - 'isAssigned': bool, - 'formList': list, - 'main_script': dict, - } - # Handle unicode type for Python 2 vs Python 3 - if sys.version_info.major < 3: - exp_data[u'name'] = (unicode, str) + # Verify the following keys and types are + # returned by the request + exp_data = { + 'isAssigned': bool, + 'formList': list, + 'main_script': dict, + } + for key in list(exp_data.keys()): + self.assertIn(key, result['data']) + self.assertIsInstance(result['data'][key], exp_data[key]) else: - exp_data[u'name'] = str - for key in list(exp_data.keys()): - self.assertIn(key, result['data']) - self.assertIsInstance(result['data'][key], exp_data[key]) + print('No Base Configlet Builder SYS_TelemetryBuilerV* found. Skipping') + time.sleep(1) def test_api_get_configlet_by_name(self): ''' Verify get_configlet_by_name @@ -2608,9 +2584,8 @@ def test_api_tags(self): # Allow pause for Workspace state to settle post submit time.sleep(1) - # Test getting new workspace post submit - # Attempt this up to three times to allow for operation to complete - # in slow environment + # Test getting new workspace post submit. Attempt this up to three + # times to allow for operation to complete in slow environment for _ in range(0, 2): result = self.api.get_workspace(new_workspace_id) self.assertIn('value', result) From f5b881174b6fde1855775cc4cff327731089d558 Mon Sep 17 00:00:00 2001 From: MattH Date: Tue, 9 May 2023 09:34:36 -0400 Subject: [PATCH 03/11] Fix: Error message spacing. --- cvprac/cvp_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index 0d901b7..8ea936e 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -710,7 +710,7 @@ def _make_request(self, req_type, url, timeout, data=None, err_str) if 'Extra data' in str(error): self.log.debug('Found multiple objects or NO objects in' - 'response data. Attempt to decode') + ' response data. Attempt to decode') decoded_data = json_decoder(response.text) return {'data': decoded_data} else: From a77e9a60802c4ad268f888d43f786579206468f3 Mon Sep 17 00:00:00 2001 From: Tamas Plugor <41957075+noredistribution@users.noreply.github.com> Date: Fri, 14 Jul 2023 19:36:15 +0100 Subject: [PATCH 04/11] Fix: ability to use device_decommissioning for unprovisioned devices (#253) --- cvprac/cvp_api.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index dc3fda1..6021537 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -572,7 +572,7 @@ def get_configlet_history(self, key, start=0, end=0): '%s&queryparam=&startIndex=%d&endIndex=%d' % (key, start, end), timeout=self.request_timeout) - def get_inventory(self, start=0, end=0, query=''): + def get_inventory(self, start=0, end=0, query='', provisioned=True): ''' Returns the a dict of the net elements known to CVP. Args: @@ -595,7 +595,7 @@ def get_inventory(self, start=0, end=0, query=''): timeout=self.request_timeout) return data['netElementList'] self.log.debug('v2 Inventory API Call') - data = self.clnt.get('/inventory/devices?provisioned=true', + data = self.clnt.get('/inventory/devices?provisioned=%s' % provisioned, timeout=self.request_timeout) containers = self.get_containers() for dev in data: @@ -1312,12 +1312,12 @@ def add_note_to_configlet(self, key, note): def sanitize_warnings(self, data): ''' Sanitize the warnings returned after validation. - + In some cases where the configlets has both errors - and warnings, CVP may split any warnings that have + and warnings, CVP may split any warnings that have `,` across multiple strings. This method concats the strings back into one string - per warning, and correct the warningCount. + per warning, and correct the warningCount. Args: data (dict): A dict that contians the result @@ -1330,11 +1330,11 @@ def sanitize_warnings(self, data): # nothing to do here, we can return as is return data # Since there may be warnings incorrectly split on - # ', ' within the warning text by CVP, we join all the + # ', ' within the warning text by CVP, we join all the # warnings together using ', ' into one large string temp_warnings = ", ".join(data['warnings']).strip() - # To split the large string again we match on the + # To split the large string again we match on the # 'at line XXX' that should indicate the end of the warning. # We capture as well the remaining \\n or whitespace and include # the extra ', ' added in the previous step in the matching criteria. @@ -2952,7 +2952,7 @@ def reset_device(self, app_name, device, create_task=True): from_id = parent_cont['key'] else: from_id = '' - + data = {'data': [{'info': info, 'infoPreview': info, 'action': 'reset', @@ -3775,8 +3775,13 @@ def device_decommissioning(self, device_id, request_id): 'deviceId': 'BAD032986065E8DC14CBB6472EC314A6'}, 'time': '2022-02-12T02:58:30.765459650Z'} ''' - device_info = self.get_device_by_serial(device_id) - if device_info is not None and 'serialNumber' in device_info: + device_exists = False + inventory = self.get_inventory(provisioned=False) + for device in inventory: + if device['serialNumber'] == device_id: + device_exists = True + break + if device_exists: msg = 'Decommissioning via Resource APIs are supported from 2021.3.0 or newer.' # For on-prem check the version as it is only supported from 2021.3.0+ if self.cvp_version_compare('>=', 7.0, msg): From 1a781c851bc566fc92288ffa666c7b86430c40de Mon Sep 17 00:00:00 2001 From: mharista Date: Wed, 19 Jul 2023 18:40:38 -0400 Subject: [PATCH 05/11] Feat: Add v9.0 apiversion and handling of new password change logout functionality in 2023.1.0 (#254) --- cvprac/cvp_client.py | 10 ++- test/system/test_cvp_client.py | 124 ++++++++++++++++++--------------- 2 files changed, 76 insertions(+), 58 deletions(-) diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index 8ea936e..a33c168 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -114,7 +114,7 @@ class CvpClient(object): # Maximum number of times to retry a get or post to the same # CVP node. NUM_RETRY_REQUESTS = 3 - LATEST_API_VERSION = 8.0 + LATEST_API_VERSION = 9.0 def __init__(self, logger='cvprac', syslog=False, filename=None, log_level='INFO'): @@ -212,7 +212,8 @@ def set_version(self, version): self.version = version self.log.info('Version %s', version) # Set apiversion to latest available API version for CVaaS - # Set apiversion to 8.0 for 2022.1.x + # Set apiversion to 9.0 for 2023.1.x + # Set apiversion to 8.0 for 2022.1.x - 2022.3.x # Set apiversion to 7.0 for 2021.3.x # Set apiversion to 6.0 for 2021.2.x # Set apiversion to 5.0 for 2020.2.4 through 2021.1.x @@ -232,7 +233,10 @@ def set_version(self, version): ' Appending 0. Updated Version String - %s', ".".join(version_components)) full_version = ".".join(version_components) - if parse_version(full_version) >= parse_version('2022.1.0'): + if parse_version(full_version) >= parse_version('2023.1.0'): + self.log.info('Setting API version to v9') + self.apiversion = 9.0 + elif parse_version(full_version) >= parse_version('2022.1.0'): self.log.info('Setting API version to v8') self.apiversion = 8.0 elif parse_version(full_version) >= parse_version('2021.3.0'): diff --git a/test/system/test_cvp_client.py b/test/system/test_cvp_client.py index 621feae..a0a0a44 100644 --- a/test/system/test_cvp_client.py +++ b/test/system/test_cvp_client.py @@ -298,52 +298,6 @@ def test_get_handle_timeout(self): with self.assertRaises(Timeout): self.clnt.get('/tasks/getTasks.do', timeout=0.0001) - def test_get_except_fail_reconnect(self): - ''' Verify exception raised if session fails and cannot be - re-established. - ''' - dut = self.duts[0] - nodes = ['bogus', dut['node']] - self.clnt.connect(nodes, dut['username'], dut['password']) - - if self.clnt.apiversion is None: - self.clnt.api.get_cvp_info() - if self.clnt.apiversion == 7.0: - pprint("Skip test case for issue in CVP 2021.3.1") - self.skipTest("Skip test case for issue in CVP 2021.3.1") - - # Change the password for the CVP user so that a session reconnect - # to any node will fail - self._change_passwd(nodes, dut['username'], dut['password'], - self.NEW_PASSWORD) - - try: - # Logout to end the current session and force a reconnect for the - # next request. - result = self.clnt.post('/login/logout.do', None) - self.assertIn('data', result) - self.assertEqual('success', result['data']) - - except Exception as error: - # Should not have had an exception. Restore the CVP password - # and re-raise the error. - self._change_passwd(nodes, dut['username'], self.NEW_PASSWORD, - dut['password']) - raise error - try: - # Try a get request and expect a CvpSessionLogOutError - result = self.clnt.get('/cvpInfo/getCvpInfo.do') - except (CvpSessionLogOutError, CvpApiError): - pass - except Exception as error: - # Unexpected error, restore password and re-raise the error. - self._change_passwd(nodes, dut['username'], self.NEW_PASSWORD, - dut['password']) - raise error - # Restore password - self._change_passwd(nodes, dut['username'], self.NEW_PASSWORD, - dut['password']) - def test_post_not_connected(self): ''' Verify post with no connection raises a ValueError ''' @@ -411,7 +365,7 @@ def test_post_cvp_url_bad(self): with self.assertRaises(CvpRequestError): self.clnt.post('/aaa/bogus.do', None) - def test_post_except_fail_reconn(self): + def test_get_except_fail_reconnect(self): ''' Verify exception raised if session fails and cannot be re-established. ''' @@ -430,19 +384,79 @@ def test_post_except_fail_reconn(self): self._change_passwd(nodes, dut['username'], dut['password'], self.NEW_PASSWORD) - try: - # Logout to end the current session and force a reconnect for the - # next request. - result = self.clnt.post('/login/logout.do', None) - self.assertIn('data', result) - self.assertEqual('success', result['data']) + msg = 'Password changes automatically logout all sessions as of' \ + 'CVP 2023.1.0. Skip logout API accordingly' + if self.clnt.api.cvp_version_compare('<', 9.0, msg): + try: + # Logout to end the current session and force a reconnect for the + # next request. + result = self.clnt.post('/login/logout.do', None) + self.assertIn('data', result) + self.assertEqual('success', result['data']) + + except Exception as error: + # Should not have had an exception. Restore the CVP password + # and re-raise the error. + self._change_passwd(nodes, dut['username'], self.NEW_PASSWORD, + dut['password']) + raise error + else: + print('Skip Logout for CVP 2023.1.0+ as the password change now results' + 'in automatic logout of the session that changed the password') + try: + # Try a get request and expect a CvpSessionLogOutError + result = self.clnt.get('/cvpInfo/getCvpInfo.do') + except (CvpSessionLogOutError, CvpApiError): + pass except Exception as error: - # Should not have had an exception. Restore the CVP password - # and re-raise the error. + # Unexpected error, restore password and re-raise the error. self._change_passwd(nodes, dut['username'], self.NEW_PASSWORD, dut['password']) raise error + # Restore password + self._change_passwd(nodes, dut['username'], self.NEW_PASSWORD, + dut['password']) + + def test_post_except_fail_reconnect(self): + ''' Verify exception raised if session fails and cannot be + re-established. + ''' + dut = self.duts[0] + nodes = ['bogus', dut['node']] + self.clnt.connect(nodes, dut['username'], dut['password']) + + if self.clnt.apiversion is None: + self.clnt.api.get_cvp_info() + if self.clnt.apiversion == 7.0: + pprint("Skip test case for issue in CVP 2021.3.1") + self.skipTest("Skip test case for issue in CVP 2021.3.1") + + # Change the password for the CVP user so that a session reconnect + # to any node will fail + self._change_passwd(nodes, dut['username'], dut['password'], + self.NEW_PASSWORD) + + msg = 'Password changes automatically logout all sessions as of' \ + 'CVP 2023.1.0. Skip logout API accordingly' + if self.clnt.api.cvp_version_compare('<', 9.0, msg): + try: + # Logout to end the current session and force a reconnect for the + # next request. + result = self.clnt.post('/login/logout.do', None) + self.assertIn('data', result) + self.assertEqual('success', result['data']) + + except Exception as error: + # Should not have had an exception. Restore the CVP password + # and re-raise the error. + self._change_passwd(nodes, dut['username'], self.NEW_PASSWORD, + dut['password']) + raise error + else: + print('Skip Logout for CVP 2023.1.0+ as the password change now results' + 'in automatic logout of the session that changed the password') + try: # Try a post request and expect a CvpSessionLogOutError result = self.clnt.post('/login/logout.do', None) From 22814d756c7258e73b09f36fb03668c86def99c4 Mon Sep 17 00:00:00 2001 From: Tamas Plugor <41957075+noredistribution@users.noreply.github.com> Date: Wed, 30 Aug 2023 23:53:13 +0100 Subject: [PATCH 06/11] Feat: add support for config validation during config assign (#255) --- cvprac/cvp_api.py | 18 +++++++++++++++++- test/system/test_cvp_client_api.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 6021537..082b3fd 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -1463,7 +1463,7 @@ def _save_topology_v2(self, data): return self.clnt.post(url, data=data, timeout=self.request_timeout) def apply_configlets_to_device(self, app_name, dev, new_configlets, - create_task=True, reorder_configlets=False): + create_task=True, reorder_configlets=False, validate=False): ''' Apply the configlets to the device. Args: @@ -1484,6 +1484,12 @@ def apply_configlets_to_device(self, app_name, dev, new_configlets, directly. Set this parameter to True only with the full list of configlets being applied to the device provided via the new_configlets parameter. + validate (bool): Defaults to False. If set to True, the function + will validate and compare the configlets to be attached and + populate the configCompareCount field in the data dict. In case + all keys are 0, ie there is no difference between designed-config + and running-config after applying the configlets, no task will be + generated. Returns: response (dict): A dict that contains a status and a list of @@ -1536,6 +1542,16 @@ def apply_configlets_to_device(self, app_name, dev, new_configlets, 'nodeTargetIpAddress': dev['ipAddress'], 'childTasks': [], 'parentTask': ''}]} + if validate: + validation_result = self.validate_configlets_for_device(dev['systemMacAddress'], ckeys) + data['data'][0].update({ + "configCompareCount": { + "mismatch": validation_result['mismatch'], + "reconcile": validation_result['reconcile'], + "new": validation_result['new'] + } + } + ) self.log.debug('apply_configlets_to_device: saveTopology data:\n%s' % data['data']) self._add_temp_action(data) diff --git a/test/system/test_cvp_client_api.py b/test/system/test_cvp_client_api.py index 98ea429..7f92e2b 100644 --- a/test/system/test_cvp_client_api.py +++ b/test/system/test_cvp_client_api.py @@ -1434,6 +1434,7 @@ def test_api_configlets_to_device(self): ''' Verify apply_configlets_to_device and remove_configlets_from_device. Also test apply_configlets_to_device with reorder_configlets parameter set to True + and test validate parameter set to True ''' # pylint: disable=too-many-statements # Create a new configlet @@ -1532,6 +1533,34 @@ def test_api_configlets_to_device(self): # Delete the new configlet self.api.delete_configlet(name, key) + # Test configlet apply with configlet validate and compare + # where the designed-config will match the running-config + # the result should yield no tasks + running_config = self.api.get_device_configuration(self.device['systemMacAddress']) + + # add new configlet and append to the list of configlets + new_configlet_name = self.device['fqdn'] + "_rc" + self.api.add_configlet(new_configlet_name, running_config) + new_configlet_data = self.api.get_configlet_by_name(new_configlet_name) + + # Apply the configlet to the device + label = 'cvprac device configlet test with validation' + validate_and_apply = self.api.apply_configlets_to_device( + label, + self.device, + [new_configlet_data], + validate=True + ) + + # Check that no taskids have been generated + self.assertEqual(validate_and_apply['data']['taskIds'], []) + self.assertEqual(validate_and_apply['data']['status'], 'success') + + # Delete the new configlet + param = {'name': new_configlet_name, 'key': new_configlet_data['key']} + self.api.remove_configlets_from_device(label, self.device, [param]) + self.api.delete_configlet(new_configlet_name, new_configlet_data['key']) + # Check compliance self.test_api_check_compliance() From 525f3a19d16a208fbdc2c1ed9fa5b27a655323f0 Mon Sep 17 00:00:00 2001 From: Tamas Plugor <41957075+noredistribution@users.noreply.github.com> Date: Thu, 31 Aug 2023 14:51:05 +0100 Subject: [PATCH 07/11] Feat: add support for config validation during config removal (#256) adding validation on config removal too to not generate empty task for no-ops --- cvprac/cvp_api.py | 18 +++++++++++++++++- test/system/test_cvp_client_api.py | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 082b3fd..c390989 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -1561,7 +1561,7 @@ def apply_configlets_to_device(self, app_name, dev, new_configlets, # pylint: disable=too-many-locals def remove_configlets_from_device(self, app_name, dev, del_configlets, - create_task=True): + create_task=True, validate=False): ''' Remove the configlets from the device. Args: @@ -1570,6 +1570,12 @@ def remove_configlets_from_device(self, app_name, dev, del_configlets, del_configlets (list): List of configlet name and key pairs create_task (bool): Determines whether or not to execute a save and create the tasks (if any) + validate (bool): Defaults to False. If set to True, the function + will validate and compare the configlets to be attached and + populate the configCompareCount field in the data dict. In case + all keys are 0, ie there is no difference between designed-config + and running-config after applying the configlets, no task will be + generated. Returns: response (dict): A dict that contains a status and a list of @@ -1628,6 +1634,16 @@ def remove_configlets_from_device(self, app_name, dev, del_configlets, 'nodeTargetIpAddress': dev['ipAddress'], 'childTasks': [], 'parentTask': ''}]} + if validate: + validation_result = self.validate_configlets_for_device(dev['systemMacAddress'], keep_keys) + data['data'][0].update({ + "configCompareCount": { + "mismatch": validation_result['mismatch'], + "reconcile": validation_result['reconcile'], + "new": validation_result['new'] + } + } + ) self.log.debug('remove_configlets_from_device: saveTopology data:\n%s' % data['data']) self._add_temp_action(data) diff --git a/test/system/test_cvp_client_api.py b/test/system/test_cvp_client_api.py index 7f92e2b..4e266c8 100644 --- a/test/system/test_cvp_client_api.py +++ b/test/system/test_cvp_client_api.py @@ -1558,7 +1558,7 @@ def test_api_configlets_to_device(self): # Delete the new configlet param = {'name': new_configlet_name, 'key': new_configlet_data['key']} - self.api.remove_configlets_from_device(label, self.device, [param]) + self.api.remove_configlets_from_device(label, self.device, [param], validate=True) self.api.delete_configlet(new_configlet_name, new_configlet_data['key']) # Check compliance From b5306b22770848a32d5927620e804bd1482932c1 Mon Sep 17 00:00:00 2001 From: Ryan Chetcuti <109683698+chetryan@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:55:12 +0100 Subject: [PATCH 08/11] FIX: Add check to connect() to ensure token works (#258) --- cvprac/cvp_client.py | 8 ++++++++ test/fixtures/cvp_nodes.yaml | 4 ++++ test/system/test_cvp_client.py | 25 +++++++++++++++++++++++++ 3 files changed, 37 insertions(+) mode change 100644 => 100755 test/system/test_cvp_client.py diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index a33c168..602f21a 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -565,6 +565,14 @@ def _set_headers_api_token(self): # Alternative to adding token to headers it can be added to # cookies as shown below. # self.cookies = {'access_token': self.api_token} + url = self.url_prefix_short + '/api/v1/rest/' + response = self.session.get(url, + cookies=self.cookies, + headers=self.headers, + timeout=self.connect_timeout, + verify=self.cert) + # Verify that the generic request was successful + self._is_good_response(response, 'Authenticate: %s' % url) def logout(self): ''' diff --git a/test/fixtures/cvp_nodes.yaml b/test/fixtures/cvp_nodes.yaml index f9e09db..47c194a 100644 --- a/test/fixtures/cvp_nodes.yaml +++ b/test/fixtures/cvp_nodes.yaml @@ -10,3 +10,7 @@ username: CvpRacTest password: AristaInnovates device: python-test-2 + # The below fields can be defined to test the token test cases. + # If they are undefined, the tests are skipped + # api_token: + # api_token_expired: diff --git a/test/system/test_cvp_client.py b/test/system/test_cvp_client.py old mode 100644 new mode 100755 index a0a0a44..df8388a --- a/test/system/test_cvp_client.py +++ b/test/system/test_cvp_client.py @@ -151,6 +151,15 @@ def test_connect_https_good(self): dut = self.duts[0] self.clnt.connect([dut['node']], dut['username'], dut['password']) + def test_connect_token_good(self): + ''' Verify https connection succeeds to a single CVP node + Uses https protocol and port with a valid token. + ''' + dut = self.duts[0] + if 'api_token' not in dut: + raise unittest.SkipTest('No API token found for DUT. Skipping test.') + self.clnt.connect([dut['node']], dut['username'], dut['password'], api_token=dut['api_token']) + def test_connect_set_request_timeout(self): ''' Verify API request timeout is set when provided to client connect method. @@ -174,6 +183,22 @@ def test_connect_password_bad(self): with self.assertRaises(CvpLoginError): self.clnt.connect([dut['node']], dut['username'], 'password') + def test_connect_token_bad(self): + ''' Verify connect fails with bad token. + ''' + dut = self.duts[0] + with self.assertRaises(CvpLoginError): + self.clnt.connect([dut['node']], dut['username'], 'password', api_token='bad_token') + + def test_connect_token_expired(self): + ''' Verify connect fails with an expired token. + ''' + dut = self.duts[0] + if 'expired_api_token' not in dut: + raise unittest.SkipTest('No Expired token given. Skipping test.') + with self.assertRaises(CvpLoginError): + self.clnt.connect([dut['node']], dut['username'], 'password', api_token=dut['api_token_expired']) + def test_connect_node_bad(self): ''' Verify connection fails to a single bogus CVP node ''' From 6545125aa8fc6e01c56d53a6b921f8c3747def76 Mon Sep 17 00:00:00 2001 From: Tamas Plugor <41957075+noredistribution@users.noreply.github.com> Date: Tue, 31 Oct 2023 14:21:29 +0000 Subject: [PATCH 09/11] docs: document CVaaS regional URLs (#259) --- README.md | 40 +++++++++++++++++++++++++++++++--------- docs/labs/README.md | 8 ++++++-- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bb71888..d74808e 100644 --- a/README.md +++ b/README.md @@ -4,25 +4,31 @@ ## Table of Contents -1. [Overview](#overview) +- [Arista Cloudvision® Portal RESTful API Client](#arista-cloudvision-portal-restful-api-client) + - [Table of Contents](#table-of-contents) + - [Overview](#overview) - [Requirements](#requirements) -1. [Installation](#installation) + - [Installation](#installation) - [Development: Run from Source](#development-run-from-source) -1. [Getting Started](#getting-started) + - [Step 1: Clone the cvprac Github repo](#step-1-clone-the-cvprac-github-repo) + - [Step 2: Check out the desired version or branch](#step-2-check-out-the-desired-version-or-branch) + - [Step 3: Install cvprac using Pip with -e switch](#step-3-install-cvprac-using-pip-with--e-switch) + - [Step 4: Install cvprac development requirements](#step-4-install-cvprac-development-requirements) + - [Getting Started](#getting-started) - [Connecting](#connecting) - [CVP On Premises](#cvp-on-premises) - [CVaaS](#cvaas) - [CVP Version Handling](#cvp-version-handling) - [Examples](#examples) -1. [Notes For API Class Usage](#notes-for-api-class-usage) + - [Notes for API Class Usage](#notes-for-api-class-usage) - [Containers](#containers) -1. [Testing](#testing) -1. [Contact or Questions](#contact-or-questions) -1. [Contributing](#contributing) + - [Testing](#testing) + - [Contact or Questions](#contact-or-questions) + - [Contributing](#contributing) - [Working With Git](#working-with-git) - [Submitting Pull Requests](#submitting-pull-requests) - [Pull Request Semantics](#pull-request-semantics) -1. [License](#license) + - [License](#license) ## Overview @@ -151,7 +157,7 @@ examples below demonstrate connecting to CVP On Premises setups. ### CVaaS CVaaS is CloudVision as a Service. Users with CVaaS must use a REST API -token for accessing CVP with REST APIs. +token (service account tokens) for accessing CVP with REST APIs. - In the case where users authenticate with CVP (CVaaS) using Oauth a - REST API token is required to be generated and used for running REST @@ -170,6 +176,22 @@ generic in this sense. If you are using the cvaas\_token parameter please convert to api\_token because the cvaas\_token parameter will be deprecated in the future. +Please note that the correct regional URL where the CVaaS tenant is deployed must be used. The following are the +cluster URLs used in production: + +| Region | URL | +|--------|-----| +| United States 1a | [www.arista.io](https://www.arista.io) | +| United States 1c| [www.cv-prod-us-central1-c.arista.io](https://www.cv-prod-us-central1-c.arista.io)| +| Canada | [www.cv-prod-na-northeast1-b.arista.io](https://www.cv-prod-na-northeast1-b.arista.io)| +| Europe West 2| [www.cv-prod-euwest-2.arista.io](https://www.cv-prod-euwest-2.arista.io)| +| Japan| [www.cv-prod-apnortheast-1.arista.io](https://www.cv-prod-apnortheast-1.arista.io)| +| Australia | [www.cv-prod-ausoutheast-1.arista.io](https://www.cv-prod-ausoutheast-1.arista.io)| + +!!! Warning + + URLs without `www` are not supported. + ### CVP Version Handling The CVP RESTful APIs often change between releases of CVP. Cvprac diff --git a/docs/labs/README.md b/docs/labs/README.md index 132ee64..d91a81a 100644 --- a/docs/labs/README.md +++ b/docs/labs/README.md @@ -5,10 +5,12 @@ to help users interact with Arista CloudVision easily and automate the provision ## Table of Contents -1. [Authentication](#authentication) +- [cvprac labs](#cvprac-labs) + - [Table of Contents](#table-of-contents) + - [Authentication](#authentication) - [Password Authentication](#password-authentication) - [Service Account Token Authentication](#service-account-token-authentication) -1. [Known Limitations](#known-limitations) + - [Known Limitations](#known-limitations) ## Authentication @@ -60,6 +62,8 @@ clnt = CvpClient() clnt.connect(nodes=['10.83.13.33'], username='',password='',api_token=token) ``` +> Note that for CVaaS the correct regional URL must be used including `www.`. Please refer to the main page's [README.md](../../README.md#cvaas) + ## Known Limitations - for any APIs that interact with EOS devices, the service account name must match the name of the username From a90277dad5118a26891fd77ef6cbacbacb5595cf Mon Sep 17 00:00:00 2001 From: MattH Date: Thu, 14 Dec 2023 11:26:22 -0500 Subject: [PATCH 10/11] Docs: Add release notes for release v1.3.2. --- docs/release-notes-1.3.2.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/release-notes-1.3.2.rst diff --git a/docs/release-notes-1.3.2.rst b/docs/release-notes-1.3.2.rst new file mode 100644 index 0000000..91958d5 --- /dev/null +++ b/docs/release-notes-1.3.2.rst @@ -0,0 +1,23 @@ +###### +v1.3.2 +###### + +2023-12-14 + +Enhancements +^^^^^^^^^^^^ + +* Add handling of new password change logout functionality in 2023.1.0. (`254 `_) [`mharista `_] +* Add support for config validation during config assign. (`255 `_) [`noredistribution `_] +* Add support for config validation during config removal. (`256 `_) [`noredistribution `_] + +Fixed +^^^^^ + +* Add ability to use device_decommissioning for unprovisioned devices. (`253 `_) [`noredistribution `_] +* Add check to connect() to ensure token works. (`258 `_) [`chetryan `_] + +Documentation +^^^^^^^^^^^^^ + +* Add documentation for CVaaS regional URLs. (`259 `_) [`noredistribution `_] From be33f011ae7ad22d9ce07ef8ac962d579781eae7 Mon Sep 17 00:00:00 2001 From: MattH Date: Thu, 14 Dec 2023 11:27:39 -0500 Subject: [PATCH 11/11] Bump: Versions for release v1.3.2. --- VERSION | 2 +- cvprac/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 6563189..1892b92 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -develop +1.3.2 diff --git a/cvprac/__init__.py b/cvprac/__init__.py index 1a0108c..8d62fa0 100644 --- a/cvprac/__init__.py +++ b/cvprac/__init__.py @@ -32,5 +32,5 @@ ''' RESTful API Client class for Cloudvision(R) Portal ''' -__version__ = 'develop' +__version__ = '1.3.2' __author__ = 'Arista Networks, Inc.'