From 0725f68cf0f86caf887a2933ffa5dcf8f9e6fdbe Mon Sep 17 00:00:00 2001 From: MattH Date: Tue, 18 Aug 2020 13:39:34 -0400 Subject: [PATCH 01/16] Bump version back to develop. --- VERSION | 2 +- cvprac/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index ee90284..6563189 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.4 +develop diff --git a/cvprac/__init__.py b/cvprac/__init__.py index e6125d4..1a0108c 100644 --- a/cvprac/__init__.py +++ b/cvprac/__init__.py @@ -32,5 +32,5 @@ ''' RESTful API Client class for Cloudvision(R) Portal ''' -__version__ = '1.0.4' +__version__ = 'develop' __author__ = 'Arista Networks, Inc.' From c2e6d9770efb5eb56e3c4519db22281f6845b6c1 Mon Sep 17 00:00:00 2001 From: Francois RIGAULT Date: Wed, 30 Sep 2020 08:24:29 +0200 Subject: [PATCH 02/16] Never fallback to HTTP in case of connection failure Current code attempts a connection with https, and upon failure (eg: a mistake in the password) falls back to http. As a consequence: - the initial error (why it failed in the first place) is lost. It makes the investigation process much difficult - the password is sent unencrypted over the network. Apparently the initial behavior of "defaulting to http" was a request for users of old CVP versions that were fine configuring their network devices over an unencrypted connection. Fix it: remove this feature entirely. --- cvprac/cvp_client.py | 12 ------------ test/unit/test_client.py | 34 +++++----------------------------- 2 files changed, 5 insertions(+), 41 deletions(-) diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index 2fbbd07..9abb2a3 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -315,18 +315,6 @@ def _create_session(self, all_nodes=False): self.url_prefix_short = ('https://%s:%d' % (host, self.port or 443)) error = self._reset_session() - if error and not self.cert: - self.log.warning('Failed to connect over https. Potentially' - ' due to an old version of CVP. Attempting' - ' fallback to http. Error: %s', error) - # Attempt http fallback if no cert file is provided. The - # intention here is that a user providing a cert file - # forces https. - self.url_prefix = ('http://%s:%d/web' - % (host, self.port or 80)) - self.url_prefix_short = ('http://%s:%d' - % (host, self.port or 80)) - error = self._reset_session() if error is None: break self.error_msg += '%s: %s\n' % (host, error) diff --git a/test/unit/test_client.py b/test/unit/test_client.py index b5e6c7c..9c060a3 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -186,44 +186,20 @@ def test_create_session_https_port(self): self.clnt._create_session(all_nodes=True) self.assertEqual(self.clnt.url_prefix, url) - def test_create_session_http_fallback(self): - """ Test a failed https connection will attempt to fallback to http. + def test_create_session_no_http_fallback(self): + """ Test a failed https connection will not attempt to fallback to http. """ self.clnt.port = None - url = 'http://1.1.1.1:80/web' - self.clnt._reset_session = Mock() - self.clnt._reset_session.side_effect = ['Failed to connect via https', - None] - self.clnt._create_session(all_nodes=True) - self.assertEqual(self.clnt.url_prefix, url) - self.assertEqual(self.clnt.error_msg, '\n') - - def test_create_session_http_fallback_port(self): - """ Test http fallback will use a user provided port number. - """ - self.clnt.port = 8888 - url = 'http://1.1.1.1:8888/web' - self.clnt._reset_session = Mock() - self.clnt._reset_session.side_effect = ['Failed to connect via https', - None] - self.clnt._create_session(all_nodes=True) - self.assertEqual(self.clnt.url_prefix, url) - self.assertEqual(self.clnt.error_msg, '\n') - - def test_create_session_no_http_fallback_with_cert(self): - """ If user passes a certificate to CvpClient it will only attempt to - use https and not fall back to http. - """ - self.clnt.port = None - self.clnt.cert = 'cert' url = 'https://1.1.1.1:443/web' error = '\n1.1.1.1: Failed to connect via https\n' self.clnt._reset_session = Mock() - self.clnt._reset_session.return_value = 'Failed to connect via https' + self.clnt._reset_session.side_effect = ['Failed to connect via https', + None] self.clnt._create_session(all_nodes=True) self.assertEqual(self.clnt.url_prefix, url) self.assertEqual(self.clnt.error_msg, error) + def test_make_request_good(self): """ Test request does not raise exception and returns json. """ From ac2318890dfd3af437411363d3b782a9d28dfac7 Mon Sep 17 00:00:00 2001 From: MattH Date: Fri, 9 Oct 2020 15:17:01 -0400 Subject: [PATCH 03/16] Add client handling for CVP 2020.3. Update get_logs_by_id for CVP 2020.3 changes. Additional test additions and fixes. --- cvprac/cvp_api.py | 93 +++++++++++++++++++++++++++++-------- cvprac/cvp_client.py | 13 ++++-- test/system/test_cvp_api.py | 58 +++++++++++++++++++---- test/unit/test_client.py | 1 - 4 files changed, 132 insertions(+), 33 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index e42a368..aac191b 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -244,11 +244,62 @@ def get_logs_by_id(self, task_id, start=0, end=0): task (dict): The CVP log for the associated Id. Returns None if the task_id was invalid. ''' - self.log.debug('get_log_by_id: task_id: %s' % task_id) - return self.clnt.get('/task/getLogsById.do?id=%s&queryparam=' - '&startIndex=%d&endIndex=%d' % - (task_id, start, end), - timeout=self.request_timeout) + self.log.debug('get_logs_by_id: task_id: %s' % task_id) + if self.clnt.apiversion is None: + self.get_cvp_info() + if self.clnt.apiversion < 5.0: + self.log.debug('v1 - v4 /task/getLogsByID.do?') + resp = self.clnt.get('/task/getLogsById.do?id=%s&queryparam=' + '&startIndex=%d&endIndex=%d' % + (task_id, start, end), + timeout=self.request_timeout) + else: + self.log.debug('v5 /audit/getLogs.do') + task_info = self.get_task_by_id(task_id) + stage_id = None + if 'stageId' in task_info: + stage_id = task_info['stageId'] + else: + self.log.debug('No stage ID found for task %s' % task_id) + if 'ccIdV2' in task_info: + cc_id = task_info['ccIdV2'] + if cc_id == '': + self.log.debug('No ccIdV2 for task %s.' + ' It was likely cancelled.' + ' Using old /task/getLogsByID.do?' + % task_id) + resp = self.clnt.get( + '/task/getLogsById.do?id=%s&queryparam=' + '&startIndex=%d&endIndex=%d' % (task_id, start, end), + timeout=self.request_timeout) + else: + resp = self.get_audit_logs_by_id(cc_id, stage_id) + else: + self.log.debug('No change ID found for task %s' % task_id) + resp = None + return resp + + def get_audit_logs_by_id(self, cc_id, stage_id=None, data_size=75): + ''' Returns the audit logs of a particular ChangeControl. + + Args: + cc_id (string): change control ID from ccIdV2 field + stage_id (string): stage ID from stageId field + data_size (int): data size + + Returns: + task (dict): The CVP log for the associated ccIdV2 + ''' + data = {"category": "ChangeControl", + "startTime": 0, + "endTime": 0, + "dataSize": data_size, + "objectKey": cc_id, + "lastRetrievedAudit": {}} + if stage_id: + data["tags"] = {"stageId": stage_id} + return self.clnt.post('/cvpservice/audit/getLogs.do?', data=data, + timeout=self.request_timeout) def add_note_to_task(self, task_id, note): ''' Add notes to the task. @@ -1454,8 +1505,8 @@ def validate_configlets_for_device(self, mac, configlet_keys, Args: mac (str): MAC address of device to validate configlets for. configlet_keys (list): List of configlet keys - page_type (list): Possible Values of pageType - - 'viewConfig', 'managementIPValidation', 'validate' etc.. + page_type (list): Possible Values of pageType - 'viewConfig', + 'managementIPValidation', 'validateConfig', etc... Returns: response (dict): A dict that contains ... @@ -2108,9 +2159,9 @@ def get_change_controls(self, query='', start=0, end=0): if self.clnt.apiversion is None: self.get_cvp_info() if self.clnt.apiversion >= 3.0: - self.log.debug('v3/v4 getChangeControls API Call') + self.log.debug('v3/v4/v5 getChangeControls API Call') self.log.warning( - 'get_change_controls: change control APIs moved for v3/v4') + 'get_change_controls: change control APIs moved for v3/v4/v5') return None self.log.debug('v2 getChangeControls API Call') @@ -2138,7 +2189,8 @@ def change_control_available_tasks(self, query='', start=0, end=0): if self.clnt.apiversion is None: self.get_cvp_info() if self.clnt.apiversion >= 3.0: - self.log.debug('v3/v4 uses existing get_task_by_status API Call') + self.log.debug( + 'v3/v4/v5 uses existing get_task_by_status API Call') return self.get_tasks_by_status('PENDING') self.log.debug('v2 getTasksByStatus API Call') @@ -2206,9 +2258,9 @@ def create_change_control(self, name, change_control_tasks, timezone, if self.clnt.apiversion is None: self.get_cvp_info() if self.clnt.apiversion >= 3.0: - self.log.debug('v3/v4 addOrUpdateChangeControl API Call') + self.log.debug('v3/v4/v5 addOrUpdateChangeControl API Call') self.log.warning('create_change_control:' - ' change control APIs moved for v3/v4') + ' change control APIs moved for v3/v4/v5') return None self.log.debug('v2 addOrUpdateChangeControl API Call') @@ -2300,9 +2352,10 @@ def add_notes_to_change_control(self, cc_id, notes): if self.clnt.apiversion is None: self.get_cvp_info() if self.clnt.apiversion >= 3.0: - self.log.debug('v3/v4 addNotesToChangeControl API Call deprecated') + self.log.debug( + 'v3/v4/v5 addNotesToChangeControl API Call deprecated') self.log.warning('add_notes_to_change_control:' - ' change control APIs not supported for v3/v4') + ' change control APIs not supported for v3/v4/v5') return None self.log.debug('v2 addNotesToChangeControl API Call') @@ -2321,7 +2374,7 @@ def execute_change_controls(self, cc_ids): self.get_cvp_info() if self.clnt.apiversion >= 3.0: self.log.debug( - 'v3/v4 /api/v3/services/ccapi.ChangeControl/Start API Call') + 'v3/v4/v5 /api/v3/services/ccapi.ChangeControl/Start API Call') for cc_id in cc_ids: resp_list = [] data = {'cc_id': cc_id} @@ -2390,7 +2443,7 @@ def cancel_change_controls(self, cc_ids): self.get_cvp_info() if self.clnt.apiversion >= 3.0: self.log.debug( - 'v3/v4 /api/v3/services/ccapi.ChangeControl/Stop API Call') + 'v3/v4/v5 /api/v3/services/ccapi.ChangeControl/Stop API Call') resp_list = [] for cc_id in cc_ids: data = {'cc_id': cc_id} @@ -2414,8 +2467,8 @@ def delete_change_controls(self, cc_ids): if self.clnt.apiversion is None: self.get_cvp_info() if self.clnt.apiversion >= 3.0: - self.log.debug( - 'v3/v4 /api/v3/services/ccapi.ChangeControl/Delete API Call') + self.log.debug('v3/v4/v5 /api/v3/services/' + 'ccapi.ChangeControl/Delete API Call') for cc_id in cc_ids: resp_list = [] data = {'cc_id': cc_id} @@ -2479,9 +2532,9 @@ def get_change_control_info(self, cc_id): self.get_cvp_info() if self.clnt.apiversion >= 3.0: self.log.debug('get_change_control_info method deprecated for' - ' v3/v4. Moved to get_change_control_status') + ' v3/v4/v5. Moved to get_change_control_status') self.log.warning('get_change_control_info:' - ' info change control API moved for v3/v4 to' + ' info change control API moved for v3/v4/v5 to' ' status') return None diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index 9abb2a3..9642d48 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -113,7 +113,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 = 4.0 + LATEST_API_VERSION = 5.0 def __init__(self, logger='cvprac', syslog=False, filename=None, log_level='INFO'): @@ -197,7 +197,8 @@ def set_version(self, version): For CVP versions 2018.1.X and prior, use api version 1.0 For CVP versions 2018.2.X, use api version 2.0 For CVP versions 2019.0.0 through 2020.1.0, use api version 3.0 - For CVP versions 2020.1.1 and later, use api version 4.0 + For CVP versions 2020.1.1 through 2020.2.X, use api version 4.0 + For CVP versions 2020.3.0 and beyond, use api version 5.0 Args: version (str): The CVP version in use. @@ -205,7 +206,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 4.0 for 2020.1.1 and beyond. + # Set apiversion to 5.0 for 2020.3.0 and beyond + # Set apiversion to 4.0 for 2020.1.1 through 2020.2.X # Set apiversion to 3.0 for 2019.0.0 through 2020.1.0 # Set apiversion to 2.0 for 2018.2.X # Set apiversion to 1.0 for 2018.1.X and prior @@ -221,7 +223,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('2020.1.1'): + if parse_version(full_version) >= parse_version('2020.3.0'): + self.log.info('Setting API version to v5') + self.apiversion = 5.0 + elif parse_version(full_version) >= parse_version('2020.1.1'): self.log.info('Setting API version to v4') self.apiversion = 4.0 elif parse_version(full_version) >= parse_version('2019.0.0'): diff --git a/test/system/test_cvp_api.py b/test/system/test_cvp_api.py index f2f090b..e8ba22f 100644 --- a/test/system/test_cvp_api.py +++ b/test/system/test_cvp_api.py @@ -366,6 +366,48 @@ def test_api_task_operations(self): # Check compliance self.test_api_check_compliance() + def test_api_get_logs(self): + ''' Verify get_logs_by_id and get_audit_logs_by_id + ''' + # pylint: disable=too-many-branches + if not self.clnt.apiversion: + self.api.get_cvp_info() + if self.clnt.apiversion < 5.0: + tasks = self.api.get_tasks() + for task in tasks['data']: + if 'workOrderId' in task: + result = self.api.get_logs_by_id(task['workOrderId']) + self.assertIsNotNone(result) + if 'data' in result: + self.assertIsNotNone(result['data']) + break + else: + tasks = self.api.get_tasks() + task_cc = None + task_no_cc = None + for task in tasks['data']: + if 'ccIdV2' in task: + if task['ccIdV2'] == '': + task_no_cc = task + else: + task_cc = task + if task_cc: + result = self.api.get_logs_by_id(task_cc['workOrderId']) + self.assertIsNotNone(result) + if 'data' in result: + self.assertIsNotNone(result['data']) + if task_no_cc: + result = self.api.get_logs_by_id(task_cc['workOrderId']) + self.assertIsNotNone(result) + if 'data' in result: + self.assertIsNotNone(result['data']) + result = self.api.get_audit_logs_by_id(task_cc['ccIdV2'], + task_cc['stageId'], + 75) + self.assertIsNotNone(result) + if 'data' in result: + self.assertIsNotNone(result['data']) + def test_api_validate_config(self): ''' Verify valid config returns True ''' @@ -804,7 +846,7 @@ def test_api_execute_task(self): ''' # Create task and execute it (task_id, _) = self._create_task() - self._execute_task(task_id) + self._execute_long_running_task(task_id) # Check compliance self.test_api_check_compliance() @@ -967,7 +1009,7 @@ def test_api_configlets_to_device(self): self.assertIn(label, result['description']) # Execute Task - self._execute_task(task_id) + self._execute_long_running_task(task_id) # Get the next task ID task_id = self._get_next_task_id() @@ -989,7 +1031,7 @@ def test_api_configlets_to_device(self): self.assertIn(label, result['description']) # Execute Task - self._execute_task(task_id) + self._execute_long_running_task(task_id) # Delete the configlet self.api.delete_configlet(name, key) @@ -1062,7 +1104,7 @@ def test_api_configlets_to_container(self): self.assertEqual(result['workOrderId'], task_id) # Execute Task - self._execute_task(task_id) + self._execute_long_running_task(task_id) # Get the next task ID task_id = self._get_next_task_id() @@ -1085,7 +1127,7 @@ def test_api_configlets_to_container(self): self.assertEqual(result['workOrderId'], task_id) # Execute Task - self._execute_task(task_id) + self._execute_long_running_task(task_id) # Delete the configlet self.api.delete_configlet(name, key) @@ -1133,7 +1175,7 @@ def test_api_validate_configlets_for_device(self): # This should result in a reconciled config with 1 new line. resp = self.api.validate_configlets_for_device(self.device['key'], compare_configlets, - 'viewConfig') + 'validateConfig') self.assertIn('reconciledConfig', resp) self.assertIn('new', resp) self.assertEqual(resp['new'], 1) @@ -1753,9 +1795,9 @@ def test_api_filter_topology(self): # image_bundle=None, create_task=True) # pprint('POST DEPLOY') # pprint(resp) - # pprint('PRE EXECTURE DEPLOY TASK') + # pprint('PRE EXECUTE DEPLOY TASK') # self._execute_long_running_task(task_id) - # pprint('POST EXECTURE DEPLOY TASK') + # pprint('POST EXECUTE DEPLOY TASK') # # final_undef_devs = self.api.get_devices_in_container('Undefined') # self.assertEqual(len(undefined_devs), len(final_undef_devs)) diff --git a/test/unit/test_client.py b/test/unit/test_client.py index 9c060a3..aac7d67 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -199,7 +199,6 @@ def test_create_session_no_http_fallback(self): self.assertEqual(self.clnt.url_prefix, url) self.assertEqual(self.clnt.error_msg, error) - def test_make_request_good(self): """ Test request does not raise exception and returns json. """ From e1ad7e813a6c7e557c27e068591ae7a9e527927f Mon Sep 17 00:00:00 2001 From: MattH Date: Mon, 12 Oct 2020 19:47:22 -0400 Subject: [PATCH 04/16] Add more detailed docstring to check_compliance function. --- cvprac/cvp_api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index aac191b..866d7a6 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -1764,8 +1764,10 @@ def check_compliance(self, node_key, node_type): applied to the device match the devices running configuration. Args: - node_key (str): The device key. - node_type (str): The device type. + node_key (str): The device key. This is the device MAC address + Example: ff:ff:ff:ff:ff:ff + node_type (str): The device type. This is either 'netelement' + or 'container' Returns: response (dict): A dict that contains the results of the From a6aa646f051eeb6cbbc5b1194a3ca6edf0558320 Mon Sep 17 00:00:00 2001 From: MattH Date: Mon, 2 Nov 2020 14:35:33 -0500 Subject: [PATCH 05/16] Update temp action system test to handle multiple temp actions created from single addTempAction in 2020.3. --- test/system/test_cvp_api.py | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/test/system/test_cvp_api.py b/test/system/test_cvp_api.py index e8ba22f..563e99a 100644 --- a/test/system/test_cvp_api.py +++ b/test/system/test_cvp_api.py @@ -1199,11 +1199,12 @@ def test_api_get_all_temp_actions(self): ''' Verify get_all_temp_actions ''' # pylint: disable=protected-access - name = 'test_configlet' - config = 'lldp timer 9' + test_configlet_name = 'test_configlet' + test_configlet_config = 'lldp timer 9' # Add a configlet - key = self._create_configlet(name, config) + test_configlet_key = self._create_configlet(test_configlet_name, + test_configlet_config) # Apply the configlet to the container data = { @@ -1219,8 +1220,8 @@ def test_api_get_all_temp_actions(self): 'fromName': '', 'toName': self.container['name'], 'toIdType': 'container', - 'configletList': [key], - 'configletNamesList': [name], + 'configletList': [test_configlet_key], + 'configletNamesList': [test_configlet_name], 'ignoreConfigletList': [], 'ignoreConfigletNamesList': [], 'configletBuilderList' : [], @@ -1234,17 +1235,25 @@ def test_api_get_all_temp_actions(self): # Request the list of temp actions result = self.api.get_all_temp_actions() - # Delete the temporary action and the configlet - self.clnt.post('//provisioning/deleteAllTempAction.do') - self.api.delete_configlet(name, key) - # Validate the results - # There should be 1 temp action - self.assertEqual(result['total'], 1) + # There should be 1 temp action for CVP versions before 2020.3.0 + if self.clnt.apiversion < 5.0: + self.assertEqual(result['total'], 1) + else: + # For CVP versions starting with CVP 2020.3.0 there will be 2 + # temp actions + self.assertEqual(result['total'], 2) + # The temp action should contain the data from the add action - for dkey in data['data'][0]: - self.assertIn(dkey, result['data'][0].keys()) - self.assertEqual(data['data'][0][dkey], result['data'][0][dkey]) + for tempaction in result['data']: + if tempaction['info'] == data['data'][0]['info']: + for dkey in data['data'][0]: + self.assertIn(dkey, tempaction.keys()) + self.assertEqual(data['data'][0][dkey], tempaction[dkey]) + + # Delete the temporary action and the configlet + self.clnt.post('//provisioning/deleteAllTempAction.do') + self.api.delete_configlet(test_configlet_name, test_configlet_key) def test_api_get_event_by_id_bad(self): ''' Verify get_event_by_id returns an error for a bad ID From a2b35cb0609957b6178c549fb6a33e6eb59eeb5e Mon Sep 17 00:00:00 2001 From: MattH Date: Wed, 4 Nov 2020 12:22:57 -0500 Subject: [PATCH 06/16] Update get_device_by_name and get_device_by_mac to use search_topology instead of get_inventory. --- cvprac/cvp_api.py | 35 +++++++++++++++++++++-------------- test/system/test_cvp_api.py | 7 ++++++- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 866d7a6..29ff983 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -809,16 +809,14 @@ def get_device_by_name(self, fqdn): otherwise returns an empty hash. ''' self.log.debug('get_device_by_name: fqdn: %s' % fqdn) - data = self.get_inventory(start=0, end=0, query=fqdn) - if data: - for netelement in data: - if netelement['fqdn'] == fqdn: - device = netelement + # data = self.get_inventory(start=0, end=0, query=fqdn) + data = self.search_topology(fqdn) + device = {} + if 'netElementList' in data: + for netelem in data['netElementList']: + if netelem['fqdn'] == fqdn: + device = netelem break - else: - device = {} - else: - device = {} return device def get_device_by_mac(self, device_mac): @@ -832,12 +830,13 @@ def get_device_by_mac(self, device_mac): otherwise returns an empty hash. ''' self.log.debug('get_device_by_mac: MAC address: %s' % device_mac) - data = self.get_inventory(start=0, end=0, query=device_mac) + # data = self.get_inventory(start=0, end=0, query=device_mac) + data = self.search_topology(device_mac) device = {} - if data: - for netelement in data: - if netelement['systemMacAddress'] == device_mac: - device = netelement + if 'netElementList' in data: + for netelem in data['netElementList']: + if netelem['systemMacAddress'] == device_mac: + device = netelem break return device @@ -1739,6 +1738,14 @@ def search_topology(self, query, start=0, end=0): 'startIndex=%d&endIndex=%d' % (qplus(query), start, end), timeout=self.request_timeout) + if 'netElementList' in data: + for device in data['netElementList']: + device['status'] = device['deviceStatus'] + device['mlagEnabled'] = device['isMLAGEnabled'] + device['danzEnabled'] = device['isDANZEnabled'] + device['parentContainerKey'] = device['parentContainerId'] + device['bootupTimestamp'] = device['bootupTimeStamp'] + device['internalBuild'] = device['internalBuildId'] return data def filter_topology(self, node_id='root', fmt='topology', diff --git a/test/system/test_cvp_api.py b/test/system/test_cvp_api.py index 563e99a..e1dee87 100644 --- a/test/system/test_cvp_api.py +++ b/test/system/test_cvp_api.py @@ -617,7 +617,12 @@ def test_api_get_device_by_name(self): ''' result = self.api.get_device_by_name(self.device['fqdn']) self.assertIsNotNone(result) - self.assertEqual(result, self.device) + for key in result: + self.assertIn(key, self.device) + # Some differences in result values between inventory + # and searchTopology. For example "no" vs "False" or + # "false" vs "False" + # self.assertEqual(result[key], self.device[key]) def test_api_get_device_configuration(self): ''' Verify get_device_configuration From 409b68d905850bd471b0355b2574cf4497579ada Mon Sep 17 00:00:00 2001 From: MattH Date: Wed, 25 Nov 2020 13:45:40 -0500 Subject: [PATCH 07/16] Add general support for using api tokens for access to REST API. --- cvprac/cvp_client.py | 84 ++++++++++++++++++++++++++++---------------- 1 file changed, 54 insertions(+), 30 deletions(-) diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index 9642d48..7ea9a97 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -146,6 +146,7 @@ def __init__(self, logger='cvprac', syslog=False, filename=None, self.is_cvaas = False self.tenant = None self.cvaas_token = None + self.api_token = None self.version = None self._last_used_node = None @@ -241,7 +242,7 @@ def set_version(self, version): def connect(self, nodes, username, password, connect_timeout=10, request_timeout=30, protocol='https', port=None, cert=False, - is_cvaas=False, tenant=None, cvaas_token=None): + is_cvaas=False, tenant=None, api_token=None, cvaas_token=None): ''' Login to CVP and get a session ID and cookie. Currently certificates are not verified if the https protocol is specified. A warning may be printed out from the requests module for this case. @@ -272,6 +273,8 @@ def connect(self, nodes, username, password, connect_timeout=10, Required if is_cvaas is enabled. cvaas_token (string): API Token to use in place of UN/PW login for CVaaS. + api_token (string): API Token to use in place of UN/PW login + for CVP 2020.3.0 and beyond. Raises: CvpLoginError: A CvpLoginError is raised if a connection @@ -297,7 +300,27 @@ def connect(self, nodes, username, password, connect_timeout=10, self.port = port self.is_cvaas = is_cvaas self.tenant = tenant - self.cvaas_token = cvaas_token + if cvaas_token is not None: + self.log.warning('The cvaas_token parameter will be deprecated' + ' soon. Please start using the api_token' + ' parameter instead. It provides the same' + ' functionality that was previously provided' + ' by cvaas_token. The api_token parameter is' + ' a more general API token parameter because' + ' using the CVP REST API via token is also' + ' available for on premises CVP as of' + ' CVP version 2020.3.0') + self.cvaas_token = cvaas_token + self.api_token = cvaas_token + if api_token is not None: + self.log.warning('Using the new api_token parameter.' + ' This will override usage of the cvaas_token' + ' parameter if both are provided. This is because' + ' api_token and cvaas_token parameters are for' + ' the same use case and api_token is more' + ' generic') + self.api_token = api_token + self.cvaas_token = api_token self._create_session(all_nodes=True) # Verify that we can connect to at least one node if not self.session: @@ -444,7 +467,11 @@ def _login(self): request failed and no session could be established to a CVP node. Destroy the class and re-instantiate. ''' - if self.is_cvaas: + # Remove any previous session id from the headers + self.headers.pop('APP_SESSION_ID', None) + if self.api_token is not None: + return self._set_headers_api_token() + elif self.is_cvaas: return self._login_cvaas() return self._login_on_prem() @@ -474,8 +501,6 @@ def _login_on_prem(self): request failed and no session could be established to a CVP node. Destroy the class and re-instantiate. ''' - # Remove any previous session id from the headers - self.headers.pop('APP_SESSION_ID', None) url = self.url_prefix + '/login/authenticate.do' response = self.session.post(url, data=json.dumps(self.authdata), @@ -513,31 +538,30 @@ def _login_cvaas(self): request failed and no session could be established to a CVP node. Destroy the class and re-instantiate. ''' - # Remove any previous session id from the headers - self.headers.pop('APP_SESSION_ID', None) - if not self.cvaas_token: - # For local CVaaS users no token is needed and the local username - # and password can be used with the below Login API. - url = (self.url_prefix_short + - '/api/v1/oauth?provider=local&next=false') - cvaas_auth = {"org": self.tenant, - "name": self.authdata['userId'], - "password": self.authdata['password']} - response = self.session.post(url, - data=json.dumps(cvaas_auth), - headers=self.headers, - timeout=self.connect_timeout, - verify=self.cert) - self._check_response_status(response, 'Authenticate: %s' % url) - self.cookies = response.cookies - # self.headers['APP_SESSION_ID'] = response.json()['sessionId'] - else: - # If using CVaaS token there is no need to run a Login API. - # Simply add the token into the headers or cookies - self.headers['Authorization'] = 'Bearer %s' % self.cvaas_token - # Alternative to adding token to headers it can be added to - # cookies as shown below. - # self.cookies = {'access_token': self.cvaas_token} + # For local CVaaS users no token is needed and the local username + # and password can be used with the below Login API. + url = (self.url_prefix_short + + '/api/v1/oauth?provider=local&next=false') + cvaas_auth = {"org": self.tenant, + "name": self.authdata['userId'], + "password": self.authdata['password']} + response = self.session.post(url, + data=json.dumps(cvaas_auth), + headers=self.headers, + timeout=self.connect_timeout, + verify=self.cert) + self._check_response_status(response, 'Authenticate: %s' % url) + self.cookies = response.cookies + + def _set_headers_api_token(self): + ''' Sets headers with API token instead of making a call to login API. + ''' + # If using an API token there is no need to run a Login API. + # Simply add the token into the headers or cookies + self.headers['Authorization'] = 'Bearer %s' % self.api_token + # Alternative to adding token to headers it can be added to + # cookies as shown below. + # self.cookies = {'access_token': self.api_token} def logout(self): ''' From 8859b9c37c2e5b3574e77d3feaf76baa2193c015 Mon Sep 17 00:00:00 2001 From: tamas Date: Mon, 14 Dec 2020 19:09:37 +0000 Subject: [PATCH 08/16] cvp_api: updating user APIs --- cvprac/cvp_api.py | 31 +++++++++++++++++++++---------- test/system/test_cvp_api.py | 20 +++++++++++++++++++- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 29ff983..0ca6316 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -103,7 +103,7 @@ def get_cvp_info(self): # pylint: disable=too-many-arguments def add_user(self, username, password, role, status, first_name, - last_name, email): + last_name, email, utype): ''' Add new local user to the CVP UI. Args: @@ -114,6 +114,7 @@ def add_user(self, username, password, role, status, first_name, first_name (str): first name of the user last_name (str): last name of the user email (str): email address of the user + user_type (str): type of AAA (Local/TACACS/RADIUS) ''' if status not in ['Enabled', 'Disabled']: self.log.error('Invalid status %s.' @@ -127,31 +128,41 @@ def add_user(self, username, password, role, status, first_name, "lastName": last_name, "password": password, "userId": username, - "userStatus": status}} + "userStatus": status, + "userType": utype}} return self.clnt.post('/user/addUser.do', data=data, timeout=self.request_timeout) - def update_user(self, username, password, status, role): + def update_user(self, username, password, role, status, first_name, + last_name, email, utype): ''' Updates username information, like - changing password, disable/enable the username - and user role. + changing password, user role, email address, names, + disable/enable the username. Args: username (str): local username on CVP password (str): password of the user - status (str): status of user (enable/disable) role (str): role of the user + status (str): state of the user (Enabled/Disabled) + first_name (str): first name of the user + last_name (str): last name of the user + email (str): email address of the user + user_type (str): type of AAA (Local/TACACS/RADIUS) ''' if status not in ['Enabled', 'Disabled']: self.log.error('Invalid status %s.' ' Status must be Enabled or Disabled.' ' Defaulting to Disabled' % status) status = 'Disabled' - data = {"user": {"userId": username, - "userType": "Local", + data = {"roles": [role], + "user": {"contactNumber": "", + "email": email, + "firstName": first_name, + "lastName": last_name, + "password": password, + "userId": username, "userStatus": status, - "password": password}, - "roles": [role]} + "userType": utype}} return self.clnt.post('/user/updateUser.do?userId={}'.format(username), data=data, timeout=self.request_timeout) diff --git a/test/system/test_cvp_api.py b/test/system/test_cvp_api.py index e1dee87..0d58fd6 100644 --- a/test/system/test_cvp_api.py +++ b/test/system/test_cvp_api.py @@ -221,12 +221,14 @@ def test_api_user_operations(self): self.assertIn('userId', result['user']) self.assertEqual(result['user']['userId'], 'test_cvp_user') initial_user_status = result['user']['userStatus'] + initial_user_email = result['user']['email'] + initial_user_type = result['user']['userType'] initial_user_role = result['roles'][0] except CvpApiError: # Test Create User result = self.api.add_user('test_cvp_user', 'test_cvp_pass', 'network-admin', 'Enabled', 'Net', - 'Op', 'test_cvp_pass@email.com') + 'Op', 'test_cvp_pass@email.com', 'Local') self.assertIsNotNone(result) self.assertIn('data', result) self.assertIn('userId', result['data']) @@ -242,6 +244,8 @@ def test_api_user_operations(self): self.assertEqual(result['user']['userId'], 'test_cvp_user') self.assertIn('userStatus', result['user']) self.assertEqual(result['user']['userStatus'], 'Enabled') + self.assertIn('userType', result['user']) + self.assertEqual(result['user']['userType'], 'Local') self.assertIsNotNone(result['roles']) self.assertEqual(result['roles'], ['network-admin']) initial_user_status = result['user']['userStatus'] @@ -257,6 +261,16 @@ def test_api_user_operations(self): else: update_user_role = 'network-admin' + if initial_user_type == 'Local': + update_user_type = 'TACACS' + else: + update_user_type = 'Local' + + if initial_user_email == 'test_cvp_pass@email.com': + update_user_email = 'test_cvp_pass2@email.com' + else: + update_user_email = 'test_cvp_pass@email.com' + # Test Update User result = self.api.update_user('test_cvp_user', 'password', update_user_status, update_user_role) @@ -270,6 +284,10 @@ def test_api_user_operations(self): self.assertEqual(result['user']['userId'], 'test_cvp_user') self.assertIn('userStatus', result['user']) self.assertEqual(result['user']['userStatus'], update_user_status) + self.assertIn('userType', result['user']) + self.assertEqual(result['user']['userType'], update_user_type) + self.assertIn('email', result['user']) + self.assertEqual(result['user']['email'], update_user_email) self.assertIsNotNone(result['roles']) self.assertEqual(result['roles'], [update_user_role]) From ae3f41a02c95b74c8bd75492adb657c40d98e1f8 Mon Sep 17 00:00:00 2001 From: tamas Date: Mon, 14 Dec 2020 19:24:21 +0000 Subject: [PATCH 09/16] cvp_api: updating test for user APIs --- test/fixtures/cvp_nodes.yaml | 2 +- test/system/test_cvp_api.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/test/fixtures/cvp_nodes.yaml b/test/fixtures/cvp_nodes.yaml index 42c764d..7dab1c2 100644 --- a/test/fixtures/cvp_nodes.yaml +++ b/test/fixtures/cvp_nodes.yaml @@ -6,4 +6,4 @@ password: AristaInnovates - node: cvp3 username: CvpRacTest - password: AristaInnovates + password: AristaInnovates \ No newline at end of file diff --git a/test/system/test_cvp_api.py b/test/system/test_cvp_api.py index 0d58fd6..0756fc2 100644 --- a/test/system/test_cvp_api.py +++ b/test/system/test_cvp_api.py @@ -223,6 +223,8 @@ def test_api_user_operations(self): initial_user_status = result['user']['userStatus'] initial_user_email = result['user']['email'] initial_user_type = result['user']['userType'] + initial_first_name = resutl['user']['firstName'] + initial_last_name = resutl['user']['lastName'] initial_user_role = result['roles'][0] except CvpApiError: # Test Create User @@ -271,9 +273,21 @@ def test_api_user_operations(self): else: update_user_email = 'test_cvp_pass@email.com' + if initial_first_name == "Net": + update_first_name = "Network" + else: + update_first_name = "Net" + + if initial_last_name == "Op": + update_last_name = "Operator" + else: + update_last_name = "Op" + # Test Update User result = self.api.update_user('test_cvp_user', 'password', - update_user_status, update_user_role) + update_user_role, update_user_status, + update_first_name, update_last_name, + update_user_email, update_user_type) self.assertIsNotNone(result) self.assertIn('data', result) self.assertEqual(result['data'], 'success') From f70057a5ba77c80f36663d8dc99651b89b06d4c8 Mon Sep 17 00:00:00 2001 From: MattH Date: Mon, 14 Dec 2020 18:09:25 -0500 Subject: [PATCH 10/16] Revert test file --- test/fixtures/cvp_nodes.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/fixtures/cvp_nodes.yaml b/test/fixtures/cvp_nodes.yaml index 7dab1c2..42c764d 100644 --- a/test/fixtures/cvp_nodes.yaml +++ b/test/fixtures/cvp_nodes.yaml @@ -6,4 +6,4 @@ password: AristaInnovates - node: cvp3 username: CvpRacTest - password: AristaInnovates \ No newline at end of file + password: AristaInnovates From ab3d3eea064e998ec434fde3376b1fe15b246ce6 Mon Sep 17 00:00:00 2001 From: MattH Date: Mon, 14 Dec 2020 18:12:51 -0500 Subject: [PATCH 11/16] Update function param to match doc string. --- cvprac/cvp_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 0ca6316..24421da 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -103,7 +103,7 @@ def get_cvp_info(self): # pylint: disable=too-many-arguments def add_user(self, username, password, role, status, first_name, - last_name, email, utype): + last_name, email, user_type): ''' Add new local user to the CVP UI. Args: @@ -129,12 +129,12 @@ def add_user(self, username, password, role, status, first_name, "password": password, "userId": username, "userStatus": status, - "userType": utype}} + "userType": user_type}} return self.clnt.post('/user/addUser.do', data=data, timeout=self.request_timeout) def update_user(self, username, password, role, status, first_name, - last_name, email, utype): + last_name, email, user_type): ''' Updates username information, like changing password, user role, email address, names, disable/enable the username. @@ -162,7 +162,7 @@ def update_user(self, username, password, role, status, first_name, "password": password, "userId": username, "userStatus": status, - "userType": utype}} + "userType": user_type}} return self.clnt.post('/user/updateUser.do?userId={}'.format(username), data=data, timeout=self.request_timeout) From 671fb033c61fa60f1ea267d77555ebb41628fe1d Mon Sep 17 00:00:00 2001 From: MattH Date: Mon, 14 Dec 2020 18:21:43 -0500 Subject: [PATCH 12/16] Fix updates to user operations test --- test/system/test_cvp_api.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/system/test_cvp_api.py b/test/system/test_cvp_api.py index 0756fc2..18ce87e 100644 --- a/test/system/test_cvp_api.py +++ b/test/system/test_cvp_api.py @@ -223,8 +223,8 @@ def test_api_user_operations(self): initial_user_status = result['user']['userStatus'] initial_user_email = result['user']['email'] initial_user_type = result['user']['userType'] - initial_first_name = resutl['user']['firstName'] - initial_last_name = resutl['user']['lastName'] + initial_first_name = result['user']['firstName'] + initial_last_name = result['user']['lastName'] initial_user_role = result['roles'][0] except CvpApiError: # Test Create User @@ -252,6 +252,10 @@ def test_api_user_operations(self): self.assertEqual(result['roles'], ['network-admin']) initial_user_status = result['user']['userStatus'] initial_user_role = result['roles'][0] + initial_user_type = result['user']['userType'] + initial_user_email = result['user']['email'] + initial_first_name = result['user']['firstName'] + initial_last_name = result['user']['lastName'] if initial_user_status == 'Enabled': update_user_status = 'Disabled' From f9920c2eac8c8cc65fbe4af16b1b17d43d559c2b Mon Sep 17 00:00:00 2001 From: MattH Date: Mon, 14 Dec 2020 18:43:30 -0500 Subject: [PATCH 13/16] Fix formatting issues --- test/system/test_cvp_api.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/system/test_cvp_api.py b/test/system/test_cvp_api.py index 18ce87e..cd8f799 100644 --- a/test/system/test_cvp_api.py +++ b/test/system/test_cvp_api.py @@ -203,6 +203,7 @@ def test_api_user_operations(self): ''' Verify get_user, add_user and update_user ''' # pylint: disable=too-many-statements + # pylint: disable=too-many-branches dut = self.duts[0] # Test Get User result = self.api.get_user(dut['username']) @@ -230,7 +231,8 @@ def test_api_user_operations(self): # Test Create User result = self.api.add_user('test_cvp_user', 'test_cvp_pass', 'network-admin', 'Enabled', 'Net', - 'Op', 'test_cvp_pass@email.com', 'Local') + 'Op', 'test_cvp_pass@email.com', + 'Local') self.assertIsNotNone(result) self.assertIn('data', result) self.assertIn('userId', result['data']) @@ -281,7 +283,7 @@ def test_api_user_operations(self): update_first_name = "Network" else: update_first_name = "Net" - + if initial_last_name == "Op": update_last_name = "Operator" else: From 5b53e467df7c92cf95b9bea6ba7c51f0917a2644 Mon Sep 17 00:00:00 2001 From: Colin MacGiollaEain <43743234+colinmacgiolla@users.noreply.github.com> Date: Mon, 25 Jan 2021 09:35:42 +0000 Subject: [PATCH 14/16] Update approve_change_control to provide timestamp Update the Change control to provide the timestamp by default. This change means that a user doesn't have to provide their own timestamp, while not breaking any existing user use cases --- cvprac/cvp_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 24421da..d459d8d 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -36,6 +36,7 @@ # This import is for proper file IO handling support for both Python 2 and 3 # pylint: disable=redefined-builtin from io import open +from datetime import datetime from cvprac.cvp_client_errors import CvpApiError @@ -2410,7 +2411,7 @@ def execute_change_controls(self, cc_ids): return self.clnt.post('/changeControl/executeCC.do', data=data, timeout=self.request_timeout) - def approve_change_control(self, cc_id, timestamp): + def approve_change_control(self, cc_id, timestamp=datetime.utcnow().isoformat()+'Z'): ''' Cancel the provided change controls. Args: From 500d9eca6e7fd42c4c795a5dd65cf787d78c3cbd Mon Sep 17 00:00:00 2001 From: MattH Date: Wed, 10 Feb 2021 16:53:08 -0500 Subject: [PATCH 15/16] Fix version handling to support changes in 2020.2.4. Fix formatting with new default variable for change controls. Fix tests for CVP 2021.1 --- cvprac/cvp_api.py | 3 ++- cvprac/cvp_client.py | 4 ++-- test/system/test_cvp_api.py | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index d459d8d..02b6e8f 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -2411,7 +2411,8 @@ def execute_change_controls(self, cc_ids): return self.clnt.post('/changeControl/executeCC.do', data=data, timeout=self.request_timeout) - def approve_change_control(self, cc_id, timestamp=datetime.utcnow().isoformat()+'Z'): + def approve_change_control(self, cc_id, + timestamp=datetime.utcnow().isoformat() + 'Z'): ''' Cancel the provided change controls. Args: diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index 7ea9a97..48fdc95 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -207,7 +207,7 @@ 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 5.0 for 2020.3.0 and beyond + # Set apiversion to 5.0 for 2020.2.4 and beyond # Set apiversion to 4.0 for 2020.1.1 through 2020.2.X # Set apiversion to 3.0 for 2019.0.0 through 2020.1.0 # Set apiversion to 2.0 for 2018.2.X @@ -224,7 +224,7 @@ 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('2020.3.0'): + if parse_version(full_version) >= parse_version('2020.2.4'): self.log.info('Setting API version to v5') self.apiversion = 5.0 elif parse_version(full_version) >= parse_version('2020.1.1'): diff --git a/test/system/test_cvp_api.py b/test/system/test_cvp_api.py index cd8f799..965630c 100644 --- a/test/system/test_cvp_api.py +++ b/test/system/test_cvp_api.py @@ -538,8 +538,16 @@ def test_api_get_configlet_builder(self): except CvpApiError as e: if 'Entity does not exist' in e.msg: # Configlet Builder for 2019.x - cfglt = self.api.get_configlet_by_name( - 'SYS_TelemetryBuilderV3') + 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 + cfglt = self.api.get_configlet_by_name( + 'SYS_TelemetryBuilderV4') + else: + raise else: raise result = self.api.get_configlet_builder(cfglt['key']) @@ -1279,11 +1287,11 @@ def test_api_get_all_temp_actions(self): result = self.api.get_all_temp_actions() # Validate the results - # There should be 1 temp action for CVP versions before 2020.3.0 + # There should be 1 temp action for CVP versions before 2020.2.4 if self.clnt.apiversion < 5.0: self.assertEqual(result['total'], 1) else: - # For CVP versions starting with CVP 2020.3.0 there will be 2 + # For CVP versions starting with CVP 2020.2.4 there will be 2 # temp actions self.assertEqual(result['total'], 2) From d9a2fedb3bb9dc6789ed9c1de63ac8a6c9c0230c Mon Sep 17 00:00:00 2001 From: MattH Date: Thu, 11 Feb 2021 11:56:35 -0500 Subject: [PATCH 16/16] Add release notes and update version for release v1.0.5 --- VERSION | 2 +- cvprac/__init__.py | 2 +- docs/release-notes-1.0.5.rst | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 docs/release-notes-1.0.5.rst diff --git a/VERSION b/VERSION index 6563189..90a27f9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -develop +1.0.5 diff --git a/cvprac/__init__.py b/cvprac/__init__.py index 1a0108c..a1cdd74 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.0.5' __author__ = 'Arista Networks, Inc.' diff --git a/docs/release-notes-1.0.5.rst b/docs/release-notes-1.0.5.rst new file mode 100644 index 0000000..9cb1b44 --- /dev/null +++ b/docs/release-notes-1.0.5.rst @@ -0,0 +1,16 @@ +###### +v1.0.5 +###### + +2021-2-11 + +Enhancements +^^^^^^^^^^^^ + +* Never fallback to HTTP in case of connection failure. (`c2e6d97 `_) [`freedge `_] +* Add client handling for CVP 2020.3. Update get_logs_by_id for CVP 2020.3. (`ac23188 `_) [`mharista `_] +* Add more detailed docstring to check_compliance function. (`e1ad7e8 `_) [`mharista `_] +* Update get_device_by_name and get_device_by_mac to use search_topology instead of get_inventory. (`a2b35cb `_) [`mharista `_] +* Add general support for using api tokens for access to REST API. (`409b68d `_) [`mharista `_] +* Updated/Enhanced user APIs. (`0c652ea `_) [`noredistribution `_] +* Update approve_change_control to provide current time timestamp as default. (`fb13861 `_) [`colinmacgiolla `_]