From bfda33ed9efbb138da390bcab2b27e887396a543 Mon Sep 17 00:00:00 2001 From: MattH Date: Fri, 20 May 2022 17:39:37 -0400 Subject: [PATCH 01/19] Bump: versions back to develop post Release v1.2.0 --- VERSION | 2 +- cvprac/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 26aaba0..6563189 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 +develop diff --git a/cvprac/__init__.py b/cvprac/__init__.py index 3b1c5de..1a0108c 100644 --- a/cvprac/__init__.py +++ b/cvprac/__init__.py @@ -32,5 +32,5 @@ ''' RESTful API Client class for Cloudvision(R) Portal ''' -__version__ = '1.2.0' +__version__ = 'develop' __author__ = 'Arista Networks, Inc.' From 5c93cf124787538b967a76c73ce797ef7c65dee5 Mon Sep 17 00:00:00 2001 From: MattH Date: Mon, 6 Jun 2022 19:04:16 -0400 Subject: [PATCH 02/19] Feat: Add function for getUsers API endpoint. --- cvprac/cvp_api.py | 31 ++++++++++++++++++++++++++++++ test/system/test_cvp_client_api.py | 18 +++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 858032b..1ffd241 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -176,6 +176,37 @@ def get_user(self, username): return self.clnt.get('/user/getUser.do?userId={}'.format(username), timeout=self.request_timeout) + def get_users(self, query='', start=0, end=0): + ''' Returns all users in CVP filtered by an optional query parameter + + Args: + query (str): Query parameter to filter users by. + start (int): Start index for the pagination. Default is 0. + end (int): End index for the pagination. If end index is 0 + then all the records will be returned. Default is 0. + + Returns: + response (dict): A dict that contains the users. + {'total': 1, + 'roles': {'cvpadmin': ['network-admin']}, + 'users': [{'userId': 'cvpadmin', + 'firstName': '', + 'lastName': '', + 'description': '', + 'email': 'cvprac@cvprac.com', + 'lastAccessed': 1654555139700, + 'contactNumber': '', + 'userType': 'Local', + 'userStatus': 'Enabled', + 'currentStatus': 'Online', + 'addedByUser': 'cvp system'}]} + ''' + self.log.debug('get_users: query: %s' % query) + return self.clnt.get('/user/getUsers.do?' + 'queryparam=%s&startIndex=%d&endIndex=%d' % + (qplus(query), start, end), + timeout=self.request_timeout) + def delete_user(self, username): ''' Remove specified user from CVP diff --git a/test/system/test_cvp_client_api.py b/test/system/test_cvp_client_api.py index 6c8dc4b..907f6bf 100644 --- a/test/system/test_cvp_client_api.py +++ b/test/system/test_cvp_client_api.py @@ -85,6 +85,12 @@ def test_api_user_operations(self): ''' # pylint: disable=too-many-statements # pylint: disable=too-many-branches + # Test Get All Users + result = self.api.get_users() + self.assertIsNotNone(result) + self.assertIn('total', result) + start_total = result['total'] + dut = self.duts[0] # Test Get User if 'username' in dut: @@ -193,6 +199,12 @@ def test_api_user_operations(self): self.assertIsNotNone(result['roles']) self.assertEqual(result['roles'], [update_user_role]) + # Test Get All Users with New User + result = self.api.get_users() + self.assertIsNotNone(result) + self.assertIn('total', result) + self.assertEqual(result['total'], start_total + 1) + # Test Delete User result = self.api.delete_user('test_cvp_user') self.assertIsNotNone(result) @@ -203,6 +215,12 @@ def test_api_user_operations(self): with self.assertRaises(CvpApiError): self.api.get_user('test_cvp_user') + # Test Get All Users Final + result = self.api.get_users() + self.assertIsNotNone(result) + self.assertIn('total', result) + self.assertEqual(result['total'], start_total) + def test_api_check_compliance(self): ''' Verify check_compliance ''' From 11675ad4a363bff7ba742966c9a14d4857c93ed3 Mon Sep 17 00:00:00 2001 From: mharista Date: Thu, 9 Jun 2022 16:44:20 -0400 Subject: [PATCH 03/19] Fix: raise error for JSON decoding when incomplete block is found (#202) * Fix: raise error for JSON decoding when incomplete block is found * Refactor: Update doc string. Check for contents before JSON decoding. Truncate long errors. * Test: Add test cases for JSON Decode handling --- cvprac/cvp_client.py | 43 ++++--- test/unit/test_client.py | 244 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 272 insertions(+), 15 deletions(-) diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index c48262f..11ba378 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -100,7 +100,7 @@ import requests from requests.exceptions import ConnectionError, HTTPError, Timeout, \ - ReadTimeout, TooManyRedirects + ReadTimeout, TooManyRedirects, JSONDecodeError from cvprac.cvp_api import CvpApi from cvprac.cvp_client_errors import CvpApiError, CvpLoginError, \ @@ -602,6 +602,11 @@ def _make_request(self, req_type, url, timeout, data=None, or delete request failed and no session could be established to a CVP node. Destroy the class and re-instantiate. + JSONDecodeError: A JSONDecodeError is raised when the response + content contains invalid JSON. Potentially in the case of + Resource APIs that will return Stream JSON format with + multiple object or in the case where the response contains + incomplete JSON. ''' # pylint: disable=too-many-branches # pylint: disable=too-many-statements @@ -658,19 +663,29 @@ def _make_request(self, req_type, url, timeout, data=None, break resp_data = None if response: - try: - resp_data = response.json() - except ValueError as error: - self.log.debug('Error trying to decode request response %s', - error) - if 'Extra data' in str(error): - self.log.debug('Found multiple objects in response data.' - 'Attempt to decode') - decoded_data = json_decoder(response.text) - resp_data = dict(data=decoded_data) - else: - self.log.debug('Attempt to return response text') - resp_data = dict(data=response.text) + if response.content: + try: + resp_data = response.json() + except JSONDecodeError as error: + err_str = str(error) + if len(err_str) > 700: + err_str = f"{err_str[:300]}[... truncated ...]" \ + f" {err_str[-300:]}" + self.log.debug( + 'Error trying to decode request response - %s', err_str) + if 'Extra data' in str(error): + self.log.debug('Found multiple objects or NO objects in' + 'response data. Attempt to decode') + decoded_data = json_decoder(response.text) + resp_data = dict(data=decoded_data) + else: + self.log.error( + 'Unknown format for JSONDecodeError - %s', err_str) + # Suppressing context as per + # https://peps.python.org/pep-0409/ + raise JSONDecodeError(err_str) from None + else: + resp_data = dict(data=[]) else: self.log.debug('Received no response for request %s %s', req_type, url) diff --git a/test/unit/test_client.py b/test/unit/test_client.py index aac7d67..c8f10df 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -36,7 +36,7 @@ import unittest from itertools import cycle from mock import Mock -from requests.exceptions import HTTPError, ReadTimeout +from requests.exceptions import HTTPError, ReadTimeout, JSONDecodeError from cvprac.cvp_client import CvpClient from cvprac.cvp_client_errors import CvpApiError, CvpSessionLogOutError @@ -217,6 +217,248 @@ def test_make_request_good(self): request_return_value.json.assert_called_once_with() self.assertEqual(self.clnt.last_used_node, '1.1.1.1') + def test_make_request_no_response(self): + """ Test handling of response being empty. + """ + self.clnt.session = Mock() + self.clnt.session.return_value = True + self.clnt.session.get.return_value = None + self.clnt._create_session = Mock() + self.clnt.NUM_RETRY_REQUESTS = 2 + self.clnt.connect_timeout = 2 + self.clnt.node_cnt = 2 + self.clnt.url_prefix = 'https://1.1.1.1:7777/web' + self.clnt._is_good_response = Mock(return_value='Good') + self.assertIsNone(self.clnt.last_used_node) + resp = self.clnt._make_request('GET', 'url', 2, {'data': 'data'}) + self.assertIsNone(resp) + self.assertEqual(self.clnt.last_used_node, '1.1.1.1') + + def test_make_request_no_response_content(self): + """ Test handling of response content being None. + """ + self.clnt.session = Mock() + self.clnt.session.return_value = True + response_mock = Mock() + response_mock.content = None + response_mock.text = None + self.clnt.session.get.return_value = response_mock + self.clnt._create_session = Mock() + self.clnt.NUM_RETRY_REQUESTS = 2 + self.clnt.connect_timeout = 2 + self.clnt.node_cnt = 2 + self.clnt.url_prefix = 'https://1.1.1.1:7777/web' + self.clnt._is_good_response = Mock(return_value='Good') + self.assertIsNone(self.clnt.last_used_node) + resp = self.clnt._make_request('GET', 'url', 2, {'data': 'data'}) + expected_response = {"data": []} + self.assertEqual(resp, expected_response) + self.assertEqual(self.clnt.last_used_node, '1.1.1.1') + + def test_make_request_empty_response_content(self): + """ Test handling of response content being empty. + """ + self.clnt.session = Mock() + self.clnt.session.return_value = True + response_mock = Mock() + response_mock.content = b'' + response_mock.text = "" + self.clnt.session.get.return_value = response_mock + self.clnt._create_session = Mock() + self.clnt.NUM_RETRY_REQUESTS = 2 + self.clnt.connect_timeout = 2 + self.clnt.node_cnt = 2 + self.clnt.url_prefix = 'https://1.1.1.1:7777/web' + self.clnt._is_good_response = Mock(return_value='Good') + self.assertIsNone(self.clnt.last_used_node) + resp = self.clnt._make_request('GET', 'url', 2, {'data': 'data'}) + expected_response = {"data": []} + self.assertEqual(resp, expected_response) + self.assertEqual(self.clnt.last_used_node, '1.1.1.1') + + def test_make_request_response_content_single_json_object(self): + """ Test handling of response being valid single JSON object. + """ + self.clnt.session = Mock() + self.clnt.session.return_value = True + response_mock = Mock() + response_mock.content = b'{"data":"success"}' + response_mock.json.return_value = {"data": "success"} + response_mock.text = '{"data":"success"}' + self.clnt.session.get.return_value = response_mock + self.clnt._create_session = Mock() + self.clnt.NUM_RETRY_REQUESTS = 2 + self.clnt.connect_timeout = 2 + self.clnt.node_cnt = 2 + self.clnt.url_prefix = 'https://1.1.1.1:7777/web' + self.clnt._is_good_response = Mock(return_value='Good') + self.assertIsNone(self.clnt.last_used_node) + resp = self.clnt._make_request('GET', 'url', 2, {'data': 'data'}) + expected_response = {"data": "success"} + self.assertEqual(resp, expected_response) + self.assertEqual(self.clnt.last_used_node, '1.1.1.1') + + def test_make_request_response_content_multi_json_object(self): + """ Test handling of response being valid multiple JSON objects for + Streaming JSON. + """ + self.clnt.session = Mock() + self.clnt.session.return_value = True + response_mock = Mock() + response_mock.content = b'{"result":{"value":{' \ + b'"key":{"workspaceId":"CVPRAC_TEST",' \ + b'"value":"TAGTESTDEV"},' \ + b'"remove":false},' \ + b'"type":"INITIAL"}}\n' \ + b'{"result":{"value":{' \ + b'"key":{"workspaceId":"CVPRAC_TEST2",' \ + b'"value":"TAGTESTINT"},' \ + b'"remove":false},' \ + b'"type":"INITIAL"}}\n' + response_mock.json.side_effect = JSONDecodeError("Extra data") + response_mock.text = '{"result":{"value":{' \ + '"key":{"workspaceId":"CVPRACT1",' \ + '"value":"T1"},' \ + '"remove":false},' \ + '"type":"I1"}}\n' \ + '{"result":{"value":{' \ + '"key":{"workspaceId":"CVPRACT2",' \ + '"value":"T2"},' \ + '"remove":false},' \ + '"type":"I2"}}\n' + self.clnt.session.get.return_value = response_mock + self.clnt._create_session = Mock() + self.clnt.NUM_RETRY_REQUESTS = 2 + self.clnt.connect_timeout = 2 + self.clnt.node_cnt = 2 + self.clnt.url_prefix = 'https://1.1.1.1:7777/web' + self.clnt._is_good_response = Mock(return_value='Good') + self.assertIsNone(self.clnt.last_used_node) + resp = self.clnt._make_request('GET', 'url', 2, {'data': 'data'}) + multi_objects = [ + {"result": {"value": {"key": {"workspaceId": "CVPRACT1", + "value": "T1"}, + "remove": False}, + "type": "I1"}}, + {"result": {"value": {"key": {"workspaceId": "CVPRACT2", + "value": "T2"}, + "remove": False}, + "type": "I2"}}] + expected_response = {"data": multi_objects} + self.assertEqual(resp, expected_response) + self.assertEqual(self.clnt.last_used_node, '1.1.1.1') + + def test_make_request_response_content_truncate_long_error(self): + """ Test handling of response being valid multiple JSON objects for + Streaming JSON with large data that causes for large error message + to be truncated + """ + self.clnt.session = Mock() + self.clnt.session.return_value = True + response_mock = Mock() + response_mock.content = b'{"result":{"value":{' \ + b'"key":{"workspaceId":"CVPRAC_TEST",' \ + b'"value":"TAGTESTDEV"},' \ + b'"remove":false},' \ + b'"type":"INITIAL"}}\n' \ + b'{"result":{"value":{' \ + b'"key":{"workspaceId":"CVPRAC_TEST2",' \ + b'"value":"TAGTESTINT"},' \ + b'"remove":false},' \ + b'"type":"INITIAL"}}\n' + long_error = 'Extra data: ' \ + '{"result":{"value":{"key":{' \ + '"workspaceId":"builtin-studios-v0.82-evpn-services"},' \ + '"createdAt":"2022-05-25T23:18:33.204Z",' \ + '"createdBy":"aerisadmin",' \ + '"lastModifiedAt":"2022-05-25T23:18:33.601Z",' \ + '"lastModifiedBy":"aerisadmin",' \ + '"state":"WORKSPACE_STATE_SUBMITTED",' \ + '"lastBuildId":"build-11b310a6bc5",' \ + '"responses":{"values":{' \ + '"build-18f4ed17-4f4d-41e6-8091-ad4b310a6bc5":' \ + '{"status":"RESPONSE_STATUS_SUCCESS",' \ + '"message":"Build build-18f4ed17-10a6bc5 finished' \ + ' successfully"},"submit-1":{"status":' \ + '"RESPONSE_STATUS_SUCCESS","message":' \ + '"Submitted successfully"}}},"ccIds":{},' \ + '"type":"INITIAL"}}{"result":{"value":{"key":' \ + '{"workspaceId":"builtin-studios1vity-monitor"}' \ + ',"createdAt":"2022-05-25T23:18:32.368Z",' \ + '"Build bui1sfully"},"}}' + response_mock.json.side_effect = JSONDecodeError(long_error) + response_mock.text = '{"result":{"value":{' \ + '"key":{"workspaceId":"CVPRACT1",' \ + '"value":"T1"},' \ + '"remove":false},' \ + '"type":"I1"}}\n' \ + '{"result":{"value":{' \ + '"key":{"workspaceId":"CVPRACT2",' \ + '"value":"T2"},' \ + '"remove":false},' \ + '"type":"I2"}}\n' + self.clnt.session.get.return_value = response_mock + self.clnt._create_session = Mock() + self.clnt.NUM_RETRY_REQUESTS = 2 + self.clnt.connect_timeout = 2 + self.clnt.node_cnt = 2 + self.clnt.url_prefix = 'https://1.1.1.1:7777/web' + self.clnt._is_good_response = Mock(return_value='Good') + self.assertIsNone(self.clnt.last_used_node) + resp = self.clnt._make_request('GET', 'url', 2, {'data': 'data'}) + multi_objects = [ + {"result": {"value": {"key": {"workspaceId": "CVPRACT1", + "value": "T1"}, + "remove": False}, + "type": "I1"}}, + {"result": {"value": {"key": {"workspaceId": "CVPRACT2", + "value": "T2"}, + "remove": False}, + "type": "I2"}}] + expected_response = {"data": multi_objects} + self.assertEqual(resp, expected_response) + self.assertEqual(self.clnt.last_used_node, '1.1.1.1') + + def test_make_request_response_content_incomplete_json_object(self): + """ Test handling of response being invalid JSON objects for + Streaming JSON. + """ + self.clnt.session = Mock() + self.clnt.session.return_value = True + response_mock = Mock() + response_mock.content = b'{"result":{"value":{' \ + b'"key":{"workspaceId":"CVPRAC_TEST",' \ + b'"value":"TAGTESTDEV"},' \ + b'"remove":false},' \ + b'"type":"INITIAL"}}\n' \ + b'{"result":{"value":{' \ + b'"key":{"workspaceId":"CVPRAC_TEST2",' \ + b'"value":"TAGTESTINT"},' \ + b'"remove":false},' \ + b'"type":"INITIAL"\n' + response_mock.json.side_effect = JSONDecodeError("Unknown") + response_mock.text = '{"result":{"value":{' \ + '"key":{"workspaceId":"CVPRACT1",' \ + '"value":"T1"},' \ + '"remove":false},' \ + '"type":"I1"}}\n' \ + '{"result":{"value":{' \ + '"key":{"workspaceId":"CVPRACT2",' \ + '"value":"T2"},' \ + '"remove":false},' \ + '"type":"I2"\n' + self.clnt.session.get.return_value = response_mock + self.clnt._create_session = Mock() + self.clnt.NUM_RETRY_REQUESTS = 2 + self.clnt.connect_timeout = 2 + self.clnt.node_cnt = 2 + self.clnt.url_prefix = 'https://1.1.1.1:7777/web' + self.clnt._is_good_response = Mock(return_value='Good') + self.assertIsNone(self.clnt.last_used_node) + with self.assertRaises(JSONDecodeError): + self.clnt._make_request('GET', 'url', 2, {'data': 'data'}) + self.assertEqual(self.clnt.last_used_node, '1.1.1.1') + def test_make_request_timeout(self): """ Test request timeout exception raised if hit on multiple nodes. """ From 3026709afd5e5d8a409ea9f437bc171bcfb467a6 Mon Sep 17 00:00:00 2001 From: Tamas Plugor <41957075+noredistribution@users.noreply.github.com> Date: Wed, 15 Jun 2022 17:25:31 +0100 Subject: [PATCH 04/19] Example: Update examples (#205) --- ...evices.py => remove_all_devices_legacy.py} | 9 ++++++ .../remove_and_decommission_device.py | 32 +++++++++++++++++++ ...> remove_devices_from_container_legacy.py} | 10 ++++++ ...ve_devices.py => remove_devices_legacy.py} | 9 ++++++ 4 files changed, 60 insertions(+) rename docs/labs/lab02-inventory-operations/{remove_all_devices.py => remove_all_devices_legacy.py} (52%) create mode 100644 docs/labs/lab02-inventory-operations/remove_and_decommission_device.py rename docs/labs/lab02-inventory-operations/{remove_devices_from_container.py => remove_devices_from_container_legacy.py} (53%) rename docs/labs/lab02-inventory-operations/{remove_devices.py => remove_devices_legacy.py} (56%) diff --git a/docs/labs/lab02-inventory-operations/remove_all_devices.py b/docs/labs/lab02-inventory-operations/remove_all_devices_legacy.py similarity index 52% rename from docs/labs/lab02-inventory-operations/remove_all_devices.py rename to docs/labs/lab02-inventory-operations/remove_all_devices_legacy.py index e49b804..f8ca8cb 100644 --- a/docs/labs/lab02-inventory-operations/remove_all_devices.py +++ b/docs/labs/lab02-inventory-operations/remove_all_devices_legacy.py @@ -18,4 +18,13 @@ for netelement in inventory: devices.append(netelement['systemMacAddress']) +# Remove devices from provisioning +# This is a legacy API call that removes the devices from Network Provisioning +# in CVP versions older than 2021.3.0, however it does not remove them from +# the Device Inventory as that requires the streaming agent (TerminAttr) to be shutdown, +# which this API does not support. +# To fully decommission a device the device_decommissioning() API can be used, which is +# supported from 2021.3.0+. +# Note that using the delete_devices() function post CVP 2021.3.0 the device will be +# automatically added back to the Undefined container. clnt.api.delete_devices(devices) diff --git a/docs/labs/lab02-inventory-operations/remove_and_decommission_device.py b/docs/labs/lab02-inventory-operations/remove_and_decommission_device.py new file mode 100644 index 0000000..16e783a --- /dev/null +++ b/docs/labs/lab02-inventory-operations/remove_and_decommission_device.py @@ -0,0 +1,32 @@ +# Copyright (c) 2022 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the COPYING file. + +from cvprac.cvp_client import CvpClient +import ssl +import uuid +import time +ssl._create_default_https_context = ssl._create_unverified_context +import requests.packages.urllib3 +requests.packages.urllib3.disable_warnings() + +# Create connection to CloudVision +clnt = CvpClient() +clnt.connect(nodes=['cvp1'], username="username", password="password") + +device_id = input("Serial number of the device to be decommissioned: ") +request_id = str(uuid.uuid4()) +clnt.api.device_decommissioning(device_id, request_id) + +# This API call will fully decommission the device, ie remove it from both +# Network Provisioning and Device Inventory (telemetry). It send an eAPI request +# to EOS to shutdown the TerminAttr daemon, waits for streaming to stop and then removes +# the device from provisioning and finally decommissions it. This operation can take a few minutes. +# Supported from CVP 2021.3.0+ and CVaaS. +decomm_status = "DECOMMISSIONING_STATUS_SUCCESS" +decomm_result = "" +while decomm_result != decomm_status: + decomm_result = clnt.api.device_decommissioning_status_get_one(request_id)['value']['status'] + time.sleep(10) + +print(decomm_result) diff --git a/docs/labs/lab02-inventory-operations/remove_devices_from_container.py b/docs/labs/lab02-inventory-operations/remove_devices_from_container_legacy.py similarity index 53% rename from docs/labs/lab02-inventory-operations/remove_devices_from_container.py rename to docs/labs/lab02-inventory-operations/remove_devices_from_container_legacy.py index c432fe8..93e0e19 100644 --- a/docs/labs/lab02-inventory-operations/remove_devices_from_container.py +++ b/docs/labs/lab02-inventory-operations/remove_devices_from_container_legacy.py @@ -15,8 +15,18 @@ # Get devices in a specific container inventory = clnt.api.get_devices_in_container("Undefined") +# Create device list devices = [] for netelement in inventory: devices.append(netelement['systemMacAddress']) +# Remove devices from provisioning +# This is a legacy API call that removes the devices from Network Provisioning +# in CVP versions older than 2021.3.0, however it does not remove them from +# the Device Inventory as that requires the streaming agent (TerminAttr) to be shutdown, +# which this API does not support. +# To fully decommission a device the device_decommissioning() API can be used, which is +# supported from 2021.3.0+. +# Note that using the delete_devices() function post CVP 2021.3.0 the device will be +# automatically added back to the Undefined container. clnt.api.delete_devices(devices) diff --git a/docs/labs/lab02-inventory-operations/remove_devices.py b/docs/labs/lab02-inventory-operations/remove_devices_legacy.py similarity index 56% rename from docs/labs/lab02-inventory-operations/remove_devices.py rename to docs/labs/lab02-inventory-operations/remove_devices_legacy.py index 3cde0b3..1f274f0 100644 --- a/docs/labs/lab02-inventory-operations/remove_devices.py +++ b/docs/labs/lab02-inventory-operations/remove_devices_legacy.py @@ -18,4 +18,13 @@ devices = ["50:08:00:a7:ca:c3","50:08:00:b1:5b:0b","50:08:00:60:c6:76", "50:08:00:25:9d:36","50:08:00:8b:ee:b1","50:08:00:8c:22:49"] +# Remove devices from provisioning +# This is a legacy API call that removes the devices from Network Provisioning +# in CVP versions older than 2021.3.0, however it does not remove them from +# the Device Inventory as that requires the streaming agent (TerminAttr) to be shutdown, +# which this API does not support. +# To fully decommission a device the device_decommissioning() API can be used, which is +# supported from 2021.3.0+. +# Note that using the delete_devices() function post CVP 2021.3.0 the device will be +# automatically added back to the Undefined container. clnt.api.delete_devices(devices) From 52a44d8a098ee25761344421b99d09eeb4d19784 Mon Sep 17 00:00:00 2001 From: Tony Reddy Goda Date: Thu, 16 Jun 2022 16:04:27 -0400 Subject: [PATCH 05/19] Doc: Update the PR semantics documentation (#206) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 67ad0f5..bb71888 100644 --- a/README.md +++ b/README.md @@ -372,6 +372,7 @@ It is strongly recommended to use one from the below: - ```Feat```: Create a capability e.g. feature, test, dependency - ```Fix```: Fix an issue e.g. bug, typo, accident, misstatement - ```Doc```: Refactor of documentation, e.g. help files +- ```Example```: Add a new example or modify an [existing one](docs/labs/) - ```Test```: Add or refactor anything regarding test, e.g add a new testCases or missing testCases - ```Refactor```: A code change that MUST be just a refactoring - ```Bump```: Increase the version of something e.g. dependency From 0edf00365a18ebd869df18cb204f2531b5557dc0 Mon Sep 17 00:00:00 2001 From: Tamas Plugor <41957075+noredistribution@users.noreply.github.com> Date: Tue, 9 Aug 2022 16:40:37 +0100 Subject: [PATCH 06/19] Feat: Add serviceaccount resource APIs (#208) * apis: add serviceaccount resource APIs * apis: add function to delete all expired tokens * add getRoles and svc account examples * add tests * fix pylint * add suggestions * refactor version checking * add suggestions * fix pep8 suggestions * fix system test * Update cvprac/cvp_api.py Co-authored-by: Tony Reddy Goda * add suggestions from last review * fix docstring and the roleID loop Co-authored-by: Tony Reddy Goda --- cvprac/cvp_api.py | 208 ++++++++++++++++++ docs/labs/lab07-aaa/create_svc_account.py | 20 ++ .../lab07-aaa/create_svc_account_token.py | 23 ++ .../delete_all_expired_svc_account_tokens.py | 16 ++ docs/labs/lab07-aaa/delete_svc_account.py | 17 ++ docs/labs/lab07-aaa/svc_account_misc.py | 34 +++ test/system/test_cvp_client_api.py | 128 ++++++++++- 7 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 docs/labs/lab07-aaa/create_svc_account.py create mode 100644 docs/labs/lab07-aaa/create_svc_account_token.py create mode 100644 docs/labs/lab07-aaa/delete_all_expired_svc_account_tokens.py create mode 100644 docs/labs/lab07-aaa/delete_svc_account.py create mode 100644 docs/labs/lab07-aaa/svc_account_misc.py diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 1ffd241..4dafcda 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -85,6 +85,17 @@ def __init__(self, clnt, request_timeout=30): self.log = clnt.log self.request_timeout = request_timeout + def check_v7(self, msg): + ''' Checking the version is above CVP 2021.3.0 + ''' + if not self.clnt.is_cvaas: + if self.clnt.apiversion is None: + self.get_cvp_info() + if self.clnt.apiversion < 7.0: + self.log.warning(msg) + return False + return True + def get_cvp_info(self): ''' Returns information about CVP. @@ -3828,3 +3839,200 @@ def device_decommissioning_status_get_all(self, status="DECOMMISSIONING_STATUS_U return None self.log.debug('v7 ' + str(url)) return self.clnt.post(url, data=payload, timeout=self.request_timeout) + + def get_roles(self): + ''' Get all the user roles in CloudVision. + Returns: + response (dict): Returns a dict that contains all the user role states.. + Ex: {'total': 7, 'roles': [{'name': 'Test Role', 'key': 'role_1599019487020581247', + 'description': 'Test'...} + ''' + url = '/role/getRoles.do?startIndex=0&endIndex=0' + return self.clnt.get(url, timeout=self.request_timeout) + + def svc_account_token_get_all(self): + ''' Get all service account token states using Resource APIs. + Supported versions: CVP 2021.3.0 or newer and CVaaS. + Returns: + response (list): Returns a list of dictionaries that contains... + Ex: [{'value': {'key': {'id': 'randomId'}, 'user': 'string', + 'description': 'string','valid_until': '2022-11-02T06:58:53Z', + 'created_by': 'string', 'last_used': None}, + 'time': '2022-05-03T15:38:53.725014447Z', 'type': 'INITIAL'}, ...] + ''' + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.check_v7(msg): + url = '/api/v3/services/arista.serviceaccount.v1.TokenService/GetAll' + self.log.debug('v7 {}'.format(url)) + return self.clnt.post(url) + + def svc_account_token_get_one(self, token_id): + ''' Get a service account token's state using Resource APIs + Supported versions: CVP 2021.3.0 or newer and CVaaS. + Returns: + response (list): Returns a list of dict that contains... + Ex: [{'value': {'key': {'id': 'randomId'}, 'user': 'string', + 'description': 'string', 'valid_until': '2022-11-02T06:58:53Z', + 'created_by': 'cvpadmin', 'last_used': None}, + 'time': '2022-05-03T15:38:53.725014447Z', 'type': 'INITIAL'}] + ''' + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.check_v7(msg): + payload = {"key": {"id": token_id}} + url = '/api/v3/services/arista.serviceaccount.v1.TokenService/GetOne' + self.log.debug('v7 {} {}'.format(url, payload)) + return self.clnt.post(url, data=payload) + + def svc_account_token_delete(self, token_id): + ''' Delete a service account token using Resource APIs. + Supported versions: CVP 2021.3.0 or newer and CVaaS. + Args: + token_id (string): The id of the service account token. + Returns: + response (list): Returns a list of dict that contains the time of deletion: + Ex: [{'key': {'id': ''}, + 'time': '2022-07-26T15:29:03.687167871Z'}] + ''' + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.check_v7(msg): + payload = {"key": {"id": token_id}} + url = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Delete' + self.log.debug('v7 {} {}'.format(url, payload)) + return self.clnt.post(url, data=payload) + + def svc_account_token_set(self, username, duration, description): + ''' Create a service account token using Resource APIs. + Supported versions: CVP 2021.3.0 or newer and CVaaS. + Args: + username (string): The service account username for which the token will be + generated. + duration (string): The validity of the service account in seconds e.g.: "20000s" + The maximum value is 1 year in seconds e.g.: "31536000s" + description (string): The description of the service account token. + Returns: + response (list): Returns a list of dict that contains the token: + Ex: [{'value': {'key': {'id': ''}, 'user': 'ansible', + 'description': 'cvprac test', + 'valid_for': '550s', 'token': ''}] + ''' + payload = {'value': {'description': description, + 'user': username, + 'valid_for': duration}} + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.check_v7(msg): + url = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Set' + self.log.debug('v7 {} {}'.format(url, payload)) + return self.clnt.post(url, data=payload) + + def svc_account_get_all(self): + ''' Get all service account states using Resource APIs. + Supported versions: CVP 2021.3.0 or newer and CVaaS. + Returns: + response (list): Returns a list of dictionaries that contains... + Ex: [{'value': {'key': {'name': 'ansible'}, 'status': 'ACCOUNT_STATUS_ENABLED', + 'description': 'lab-tests', 'groups': {'values': ['network-admin']}}, + 'time': '2022-02-10T04:28:14.251684869Z', 'type': 'INITIAL'}, ...] + + ''' + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.check_v7(msg): + url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/GetAll' + self.log.debug('v7 {} '.format(url)) + return self.clnt.post(url) + + def svc_account_get_one(self, username): + ''' Get a service account's state using Resource APIs + Supported versions: CVP 2021.3.0 or newer and CVaaS. + Args: + username (string): The service account username. + Returns: + response (list): Returns a list of dict that contains... + Ex: [{'value': {'key': {'name': 'ansible'}, 'status': 'ACCOUNT_STATUS_ENABLED', + 'description': 'lab-tests', 'groups': {'values': ['network-admin']}}, + 'time': '2022-02-10T04:28:14.251684869Z'}] + ''' + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.check_v7(msg): + payload = {"key": {"name": username}} + url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/GetOne' + self.log.debug('v7 {} {}'.format(url, payload)) + return self.clnt.post(url, data=payload) + + def svc_account_set(self, username, description, roles, status): + ''' Create a service account using Resource APIs. + Supported versions: CVP 2021.3.0 or newer and CVaaS. + Args: + username (string): The service account username. + description (string): The description of the service account. + roles (list): The list of role IDs. Default roles have a human readable name, + e.g.: 'network-admin', 'network-operator'; + other roles will have the format of 'role_', + e.g. 'role_1658850344592739349'. + cvprac automatically converts non-default role names to role IDs. + status (enum): The status of the service account. Possible values: + 0 or 'ACCOUNT_STATUS_UNSPECIFIED' + 1 or 'ACCOUNT_STATUS_ENABLED' + 2 or 'ACCOUNT_STATUS_DISABLED' + Returns: + response (list): Returns a list of dict that contains... + Ex: [{'value': {'key': {'name': 'cvprac2'}, 'status': 'ACCOUNT_STATUS_ENABLED', + 'description': 'testapi', 'groups': {'values': + ['network-admin', 'role_1658850344592739349']}}, + 'time': '2022-07-26T18:19:55.392173445Z'}] + ''' + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.check_v7(msg): + role_ids = [] + all_roles = self.get_roles() + for role in all_roles['roles']: + if role['key'] in roles or role['name'] in roles: + role_ids.append(role['key']) + if len(roles) != len(role_ids): + self.log.warning('Not all provided roles {} are valid. ' + 'Only using the found valid roles {}'.format(roles, role_ids)) + + payload = {'value': {'description': description, + 'groups': {'values': role_ids}, + 'key': {'name': username}, + 'status': status}} + url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/Set' + self.log.debug('v7 {} {}'.format(url, payload)) + return self.clnt.post(url, data=payload) + + def svc_account_delete(self, username): + ''' Delete a service account using Resource APIs. + Supported versions: CVP 2021.3.0 or newer and CVaaS. + Args: + username (string): The service account username. + Returns: + response (list): Returns a list of dict that contains the time of deletion: + Ex: [{'key': {'name': 'cvprac2'}, + 'time': '2022-07-26T18:26:53.637425846Z'}] + ''' + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.check_v7(msg): + payload = {"key": {"name": username}} + url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/Delete' + self.log.debug('v7 {} {}'.format(url, payload)) + return self.clnt.post(url, data=payload) + + def svc_account_delete_expired_tokens(self): + ''' Delete all service account tokens using Resource APIs. + Supported versions: CVP 2021.3.0 or newer and CVaaS. + Returns: + response (list): Returns a list of dict that contains the list of tokens + that were deleted: + Ex: [{'value': {'key': {'id': '091f48a2808'},'user': 'cvprac3', + 'description': 'cvprac test', 'valid_until': '2022-07-26T18:31:18Z', + 'created_by': 'cvpadmin', 'last_used': None}, + 'time': '2022-07-26T18:30:28.022504853Z','type': 'INITIAL'}, + {'value': {'key': {'id': '2f6325d9c'},...] + ''' + tokens = self.svc_account_token_get_all() + expired_tokens = [] + for tok in tokens: + token = tok['value'] + if datetime.strptime(token['valid_until'], "%Y-%m-%dT%H:%M:%SZ") < datetime.utcnow(): + self.svc_account_token_delete(token['key']['id']) + expired_tokens.append(tok) + return expired_tokens diff --git a/docs/labs/lab07-aaa/create_svc_account.py b/docs/labs/lab07-aaa/create_svc_account.py new file mode 100644 index 0000000..7f0e55e --- /dev/null +++ b/docs/labs/lab07-aaa/create_svc_account.py @@ -0,0 +1,20 @@ +# Copyright (c) 2021 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the COPYING file. + +from cvprac.cvp_client import CvpClient +from cvprac.cvp_client_errors import CvpApiError +import ssl +ssl._create_default_https_context = ssl._create_unverified_context +import requests.packages.urllib3 +requests.packages.urllib3.disable_warnings() + +# Create connection to CloudVision using user/password (on-prem only) +clnt = CvpClient() +clnt.connect(['cvp1'],'username', 'password') + +username = "cvprac2" +description = "test cvprac" +roles = ["network-admin", "clouddeploy"] # both role names and role IDs are supported +status = 1 # 1 is equivalent to "ACCOUNT_STATUS_ENABLED" +clnt.api.svc_account_set(username, description, roles, status) diff --git a/docs/labs/lab07-aaa/create_svc_account_token.py b/docs/labs/lab07-aaa/create_svc_account_token.py new file mode 100644 index 0000000..4be8185 --- /dev/null +++ b/docs/labs/lab07-aaa/create_svc_account_token.py @@ -0,0 +1,23 @@ +# Copyright (c) 2021 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the COPYING file. + +from cvprac.cvp_client import CvpClient +from cvprac.cvp_client_errors import CvpApiError +import ssl +ssl._create_default_https_context = ssl._create_unverified_context +import requests.packages.urllib3 +requests.packages.urllib3.disable_warnings() + +# Create connection to CloudVision using user/password (on-prem only) +clnt = CvpClient() +clnt.connect(['cvp1'],'username', 'password') + +username = "cvprac2" +duration = "31536000s" # 1 year validity +description = "test cvprac" +svc_token = clnt.api.svc_account_token_set(username, duration, description) + +# Write the token to file in .tok format +with open(svc_token[0]['value']['user'] + ".tok", "w") as f: + f.write(svc_token[0]['value']['token']) diff --git a/docs/labs/lab07-aaa/delete_all_expired_svc_account_tokens.py b/docs/labs/lab07-aaa/delete_all_expired_svc_account_tokens.py new file mode 100644 index 0000000..68e82a9 --- /dev/null +++ b/docs/labs/lab07-aaa/delete_all_expired_svc_account_tokens.py @@ -0,0 +1,16 @@ +# Copyright (c) 2021 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the COPYING file. + +from cvprac.cvp_client import CvpClient +from cvprac.cvp_client_errors import CvpApiError +import ssl +ssl._create_default_https_context = ssl._create_unverified_context +import requests.packages.urllib3 +requests.packages.urllib3.disable_warnings() + +# Create connection to CloudVision using user/password (on-prem only) +clnt = CvpClient() +clnt.connect(['cvp1'],'username', 'password') + +clnt.api.svc_account_delete_expired_tokens() diff --git a/docs/labs/lab07-aaa/delete_svc_account.py b/docs/labs/lab07-aaa/delete_svc_account.py new file mode 100644 index 0000000..a6f7854 --- /dev/null +++ b/docs/labs/lab07-aaa/delete_svc_account.py @@ -0,0 +1,17 @@ +# Copyright (c) 2021 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the COPYING file. + +from cvprac.cvp_client import CvpClient +from cvprac.cvp_client_errors import CvpApiError +import ssl +ssl._create_default_https_context = ssl._create_unverified_context +import requests.packages.urllib3 +requests.packages.urllib3.disable_warnings() + +# Create connection to CloudVision using user/password (on-prem only) +clnt = CvpClient() +clnt.connect(['cvp1'],'username', 'password') + +username = "cvprac2" +clnt.api.svc_account_delete(username) diff --git a/docs/labs/lab07-aaa/svc_account_misc.py b/docs/labs/lab07-aaa/svc_account_misc.py new file mode 100644 index 0000000..d3eccca --- /dev/null +++ b/docs/labs/lab07-aaa/svc_account_misc.py @@ -0,0 +1,34 @@ +# Copyright (c) 2021 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the COPYING file. + +from cvprac.cvp_client import CvpClient +from cvprac.cvp_client_errors import CvpApiError +import ssl +ssl._create_default_https_context = ssl._create_unverified_context +import requests.packages.urllib3 +requests.packages.urllib3.disable_warnings() + +# Create connection to CloudVision using user/password (on-prem only) +clnt = CvpClient() +clnt.connect(['cvp1'],'username', 'password') + +# Get all service accounts states + +accounts = clnt.api.svc_account_get_all() + +# Get specific service account state + +account = clnt.api.svc_account_get_one("cvprac2") + +# Get all service account token states + +tokens = clnt.api.svc_account_token_get_all() + +# Get specific token state + +token = clnt.api.svc_account_token_get_one("9bfb39ff892c81d6ac9f25ff95d0389719595feb") + +# Delete a service account token + +clnt.api.svc_account_token_delete("9bfb39ff892c81d6ac9f25ff95d0389719595feb") diff --git a/test/system/test_cvp_client_api.py b/test/system/test_cvp_client_api.py index 907f6bf..58184c0 100644 --- a/test/system/test_cvp_client_api.py +++ b/test/system/test_cvp_client_api.py @@ -58,7 +58,7 @@ import urllib3 from test_cvp_base import TestCvpClientBase from requests.exceptions import Timeout -from cvprac.cvp_client_errors import CvpApiError +from cvprac.cvp_client_errors import CvpApiError, CvpRequestError urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) @@ -221,6 +221,132 @@ def test_api_user_operations(self): self.assertIn('total', result) self.assertEqual(result['total'], start_total) + def test_api_svc_account_operations(self): + ''' Verify svc_account_get_all, svc_account_get_one, + svc_account_set, svc_account_delete + ''' + result = self.api.svc_account_get_all() + self.assertIsNotNone(result) + start_total = len(result) + + username = "cvpractest" + description = "cvprac test" + # TODO add custom roles after role creation APIs are added + roles = ["network-admin", "network-operator"] + status = 1 # enabled + # Test get service account + try: + result = self.api.svc_account_get_one(username) + self.assertIsNotNone(result) + self.assertIn('value', result[0]) + self.assertIn('key', result[0]['value']) + self.assertIn('name', result[0]['value']['key']) + self.assertEqual(result[0]['value']['key']['name'], username) + initial_acc_status = result[0]['value']['status'] + initial_groups = result[0]['value']['groups']['values'] + except CvpRequestError: + # Test create service account + result = self.api.svc_account_set(username, description, roles, status) + self.assertIsNotNone(result) + self.assertIn('value', result[0]) + self.assertIn('key', result[0]['value']) + self.assertIn('name', result[0]['value']['key']) + self.assertEqual(result[0]['value']['key']['name'], username) + self.assertEqual(result[0]['value']['status'], 'ACCOUNT_STATUS_ENABLED') + self.assertEqual(result[0]['value']['description'], description) + self.assertEqual(result[0]['value']['groups']['values'], roles) + initial_acc_status = result[0]['value']['status'] + initial_groups = result[0]['value']['groups']['values'] + + if initial_acc_status == 'ACCOUNT_STATUS_ENABLED': + update_acc_status = 'ACCOUNT_STATUS_DISABLED' + + if initial_groups == ["network-admin", "network-operator"]: + update_groups = ["network-operator"] + # Test update service account + result = self.api.svc_account_set(username, description, update_groups, + update_acc_status) + + # Test Get all service account with new account + result = self.api.svc_account_get_all() + self.assertIsNotNone(result) + self.assertEqual(len(result), start_total + 1) + + # Test delete service account + result = self.api.svc_account_delete(username) + self.assertIsNotNone(result) + self.assertIn('key', result[0]) + self.assertIn('name', result[0]['key']) + self.assertIn('time', result[0]) + + # Verify the service account was successfully deleted and doesn't exist + with self.assertRaises(CvpRequestError): + self.api.svc_account_get_one(username) + + # Test Get All service accounts final + result = self.api.svc_account_get_all() + self.assertIsNotNone(result) + self.assertEqual(len(result), start_total) + + def test_api_svc_account_token_operations(self): + ''' Verify svc_account_set, svc_account_token_get_all, svc_account_token_set, + svc_account_token_delete, svc_account_delete_expired_tokens + ''' + # Test creating tokens + # Create a few service accounts and several tokens for each + result = self.api.svc_account_set("cvprac1", "test", ["network-admin"], 1) + self.assertIsNotNone(result) + self.assertIn('name', result[0]['value']['key']) + result = self.api.svc_account_set("cvprac2", "test", ["network-admin"], 1) + self.assertIsNotNone(result) + self.assertIn('name', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac1", "10s", "test1") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac1", "5s", "test1") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac1", "1600s", "test1") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac2", "10s", "test2") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac2", "5s", "test2") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac2", "1600s", "test2") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac2", "3600s", "test2") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + token_id = result[0]['value']['key']['id'] + + # Test Get All service account tokens + + result = self.api.svc_account_token_get_all() + self.assertIsNotNone(result) + start_total_tok = len(result) + + # Test delete a service account token + result = self.api.svc_account_token_delete(token_id) + self.assertIsNotNone(result) + self.assertIn('id', result[0]['key']) + self.assertIn('time', result[0]) + total_tok_post_del_one = self.api.svc_account_token_get_all() + self.assertEqual(start_total_tok - 1, len(total_tok_post_del_one)) + + # Test delete all expired service account tokens + + time.sleep(11) # Sleep for 11 seconds so that few of the tokens can expire + result = self.api.svc_account_delete_expired_tokens() + self.assertIsNotNone(result) + result = self.api.svc_account_token_get_all() + self.assertIsNotNone(result) + end_total_tok = len(result) + self.assertEqual(end_total_tok, start_total_tok - 5) + def test_api_check_compliance(self): ''' Verify check_compliance ''' From 641ca5eff45ca4d69318b2422119cca43a3b548d Mon Sep 17 00:00:00 2001 From: mharista Date: Tue, 9 Aug 2022 16:20:36 -0400 Subject: [PATCH 07/19] Dev tools updates (#209) --- MANIFEST.in | 2 +- dev-requirements.txt | 1 + setup.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index f9f7ef8..1942977 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include README.rst +include README.md include Makefile include *.spec include *.txt diff --git a/dev-requirements.txt b/dev-requirements.txt index 4d55592..2395cb7 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,3 +6,4 @@ pep8 pyflakes pylint pyyaml +twine diff --git a/setup.py b/setup.py index f7fb644..e691c18 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,6 @@ """ import io from os import path, walk -from glob import glob try: from setuptools import setup From 156fdf82da1e39f5b19ed5a2a82a89044781250a Mon Sep 17 00:00:00 2001 From: MattH Date: Fri, 9 Sep 2022 08:40:23 -0400 Subject: [PATCH 08/19] Fix: remove dependency for existing service account from test --- test/system/test_cvp_client_api.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/test/system/test_cvp_client_api.py b/test/system/test_cvp_client_api.py index 58184c0..4dd7e8e 100644 --- a/test/system/test_cvp_client_api.py +++ b/test/system/test_cvp_client_api.py @@ -226,8 +226,9 @@ def test_api_svc_account_operations(self): svc_account_set, svc_account_delete ''' result = self.api.svc_account_get_all() - self.assertIsNotNone(result) - start_total = len(result) + start_total = 0 + if result is not None: + start_total = len(result) username = "cvpractest" description = "cvprac test" @@ -285,8 +286,10 @@ def test_api_svc_account_operations(self): # Test Get All service accounts final result = self.api.svc_account_get_all() - self.assertIsNotNone(result) - self.assertEqual(len(result), start_total) + if result is not None: + self.assertEqual(len(result), start_total) + else: + self.assertEqual(0, start_total) def test_api_svc_account_token_operations(self): ''' Verify svc_account_set, svc_account_token_get_all, svc_account_token_set, From 2fcf3d2ea409ede5ff1389fccfced773d1806d54 Mon Sep 17 00:00:00 2001 From: MattH Date: Wed, 14 Sep 2022 12:29:33 -0400 Subject: [PATCH 09/19] Test: Add new telemetry configlet builder version for CVP 2022.2.0. Add short wait for Workspace submit to settle. --- test/system/test_cvp_client_api.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/system/test_cvp_client_api.py b/test/system/test_cvp_client_api.py index 4dd7e8e..401b2bd 100644 --- a/test/system/test_cvp_client_api.py +++ b/test/system/test_cvp_client_api.py @@ -595,9 +595,15 @@ def test_api_get_configlet_builder(self): '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') + # 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 + + cfglt = self.api.get_configlet_by_name( + 'SYS_TelemetryBuilderV5') else: raise else: @@ -2354,6 +2360,9 @@ def test_api_tags(self): self.assertEqual( response['value']['requestParams']['requestId'], new_submit_id) + # Allow pause for Workspace state to settle post submit + time.sleep(1) + # Test getting new workspace post submit result = self.api.get_workspace(new_workspace_id) self.assertIn('value', result) From dbcc0472a29f464c272164f4db54866005d78fe5 Mon Sep 17 00:00:00 2001 From: Tamas Plugor <41957075+noredistribution@users.noreply.github.com> Date: Wed, 14 Sep 2022 19:59:13 +0100 Subject: [PATCH 10/19] Example: Add example on how to automatically reconcile configurations (#212) --- .../auto_reconcile_on_rc_change.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/labs/lab06-provisioning/auto_reconcile_on_rc_change.py diff --git a/docs/labs/lab06-provisioning/auto_reconcile_on_rc_change.py b/docs/labs/lab06-provisioning/auto_reconcile_on_rc_change.py new file mode 100644 index 0000000..cff820d --- /dev/null +++ b/docs/labs/lab06-provisioning/auto_reconcile_on_rc_change.py @@ -0,0 +1,64 @@ +# Copyright (c) 2022 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the COPYING file. + +# This script can be run as a cronjob to periodically reconcile all devices +# that are out of configuration compliance in environments where the running-config +# is still modified via the CLI often. +from cvprac.cvp_client import CvpClient +import ssl +from datetime import datetime +ssl._create_default_https_context = ssl._create_unverified_context +import requests.packages.urllib3 +requests.packages.urllib3.disable_warnings() +clnt = CvpClient() +clnt.set_log_level(log_level='WARNING') + +# Reading the service account token from a file +with open("token.tok") as f: + token = f.read().strip('\n') + +clnt = CvpClient() +clnt.connect(nodes=['cvp1'], username='',password='',api_token=token) + +inventory = clnt.api.get_inventory() + +compliance = {"0001": "Config is out of sync", + "0003": "Config & image out of sync", + "0004": "Config, Image and Device time are in sync", + "0005": "Device is not reachable", + "0008": "Config, Image and Extensions are out of sync", + "0009": "Config and Extensions are out of sync", + "0012": "Config, Image, Extension and Device time are out of sync", + "0013": "Config, Image and Device time are out of sync", + "0014": "Config, Extensions and Device time are out of sync", + "0016": "Config and Device time are out of sync" + } + +non_compliants = [] +taskIds = [] +for device in inventory: + if device['complianceCode'] in compliance.keys(): + # create a list of non-compliant devices for reporting purposes + non_compliants.append(device['hostname']) + dev_mac = device['systemMacAddress'] + # check if device already has reconciled config and save the key if it does + try: + configlets = clnt.api.get_configlets_by_device_id(dev_mac) + for configlet in configlets: + if configlet['reconciled'] == True: + configlet_key = configlet['key'] + break + else: + configlet_key = "" + rc = clnt.api.get_device_configuration(dev_mac) + name = 'RECONCILE_' + device['serialNumber'] + update = clnt.api.update_reconcile_configlet(dev_mac, rc, configlet_key, name, True) + # if the device had no reconciled config, it means we need to append the reconciled + # configlet to the list of applied configlets on the device + if configlet_key == "": + addcfg = clnt.api.apply_configlets_to_device("auto-reconciling",device,[update['data']]) + clnt.api.cancel_task(addcfg['data']['taskIds'][0]) + except Exception as e: + continue +print(f"The non compliant devices were: {str(non_compliants)}") From c7f0bcf101e457988dd7b1f1874db71a37426284 Mon Sep 17 00:00:00 2001 From: MattH Date: Thu, 15 Sep 2022 19:26:46 -0400 Subject: [PATCH 11/19] Refactor: log message --- cvprac/cvp_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 4dafcda..fa23851 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -2707,8 +2707,9 @@ 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/v5 /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} From d7c8d91205ace141c6c1d4fe6be1de13682632cc Mon Sep 17 00:00:00 2001 From: Tamas Plugor <41957075+noredistribution@users.noreply.github.com> Date: Thu, 22 Sep 2022 16:50:31 +0100 Subject: [PATCH 12/19] Update get_running_configs_by_time.py (#216) --- .../get_running_configs_by_time.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py b/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py index 3b90d70..7bbc294 100644 --- a/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py +++ b/docs/labs/lab02-inventory-operations/get_running_configs_by_time.py @@ -23,8 +23,12 @@ } } try: - resultRunningConfig = clnt.post(uri, data=data)[0]['config'] + resultRunningConfig = clnt.post(uri, data=data) + for idx in resultRunningConfig: + if 'config' in idx: + result = idx['config'] + break with open(device['hostname']+'.cfg','w') as f: - f.write(resultRunningConfig) + f.write(result) except Exception as e: print("Not able to get configuration for device {} - exception {}".format(device['fqdn'], e)) From 78b0d45f250fd29c8ca90cb1a2646e0e2ab171cc Mon Sep 17 00:00:00 2001 From: mharista Date: Mon, 26 Sep 2022 16:59:50 -0400 Subject: [PATCH 13/19] Fix: issue with resource APIs return data not being wrapped with "data" key for single object (#215) --- cvprac/cvp_client.py | 70 ++++++++++++++++++------------ test/system/test_cvp_client_api.py | 19 +++++--- test/unit/test_client.py | 8 ++-- 3 files changed, 62 insertions(+), 35 deletions(-) diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index 11ba378..8a77fae 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -661,35 +661,51 @@ def _make_request(self, req_type, url, timeout, data=None, raise error continue break - resp_data = None - if response: - if response.content: - try: - resp_data = response.json() - except JSONDecodeError as error: - err_str = str(error) - if len(err_str) > 700: - err_str = f"{err_str[:300]}[... truncated ...]" \ - f" {err_str[-300:]}" - self.log.debug( - 'Error trying to decode request response - %s', err_str) - if 'Extra data' in str(error): - self.log.debug('Found multiple objects or NO objects in' - 'response data. Attempt to decode') - decoded_data = json_decoder(response.text) - resp_data = dict(data=decoded_data) - else: - self.log.error( - 'Unknown format for JSONDecodeError - %s', err_str) - # Suppressing context as per - # https://peps.python.org/pep-0409/ - raise JSONDecodeError(err_str) from None - else: - resp_data = dict(data=[]) - else: + + if not response: self.log.debug('Received no response for request %s %s', req_type, url) - return resp_data + return None + + # Added check for response.content being 'null' because of the + # service account APIs being a special case /services/ API that + # returns a null string for no objects instead of an empty string. + if not response.content or response.content == b'null': + return {'data': []} + + try: + resp_data = response.json() + if (resp_data is not None and 'result' in resp_data + and '/resources/' in full_url): + # Resource APIs use JSON streaming and will return + # multiple JSON objects during GetAll type API + # calls. We are wrapping the multiple objects into + # a key "data" and we also return a dictionary with + # key "data" as an empty dict for no data. This + # checks and keeps consistent the "data" key wrapper + # for a Resource API GetAll that returns a single + # object. + return {'data': [resp_data]} + return resp_data + except JSONDecodeError as error: + # Truncate long error messages + err_str = str(error) + if len(err_str) > 700: + err_str = f"{err_str[:300]}[... truncated ...]" \ + f" {err_str[-300:]}" + self.log.debug('Error trying to decode request response - %s', + err_str) + if 'Extra data' in str(error): + self.log.debug('Found multiple objects or NO objects in' + 'response data. Attempt to decode') + decoded_data = json_decoder(response.text) + return {'data': decoded_data} + else: + self.log.error('Unknown format for JSONDecodeError - %s', + err_str) + # Suppressing context as per + # https://peps.python.org/pep-0409/ + raise JSONDecodeError(err_str) from None def _send_request(self, req_type, full_url, timeout, data=None, files=None): diff --git a/test/system/test_cvp_client_api.py b/test/system/test_cvp_client_api.py index 401b2bd..5499c55 100644 --- a/test/system/test_cvp_client_api.py +++ b/test/system/test_cvp_client_api.py @@ -228,13 +228,16 @@ def test_api_svc_account_operations(self): result = self.api.svc_account_get_all() start_total = 0 if result is not None: - start_total = len(result) + if 'data' in result: + start_total = len(result['data']) + else: + start_total = len(result) username = "cvpractest" description = "cvprac test" # TODO add custom roles after role creation APIs are added roles = ["network-admin", "network-operator"] - status = 1 # enabled + status = 1 # enabled # Test get service account try: result = self.api.svc_account_get_one(username) @@ -259,14 +262,17 @@ def test_api_svc_account_operations(self): initial_acc_status = result[0]['value']['status'] initial_groups = result[0]['value']['groups']['values'] + update_acc_status = 'ACCOUNT_STATUS_ENABLED' if initial_acc_status == 'ACCOUNT_STATUS_ENABLED': update_acc_status = 'ACCOUNT_STATUS_DISABLED' + update_groups = ["network-admin", "network-operator"] if initial_groups == ["network-admin", "network-operator"]: update_groups = ["network-operator"] # Test update service account result = self.api.svc_account_set(username, description, update_groups, - update_acc_status) + update_acc_status) + self.assertIsNotNone(result) # Test Get all service account with new account result = self.api.svc_account_get_all() @@ -287,7 +293,10 @@ def test_api_svc_account_operations(self): # Test Get All service accounts final result = self.api.svc_account_get_all() if result is not None: - self.assertEqual(len(result), start_total) + if 'data' in result: + self.assertEqual(len(result['data']), start_total) + else: + self.assertEqual(len(result), start_total) else: self.assertEqual(0, start_total) @@ -2238,7 +2247,7 @@ def test_api_tags(self): # Test getting device tags for workspace result = self.api.get_all_tags(element_type="ELEMENT_TYPE_DEVICE", workspace_id=new_workspace_id) - self.assertNotIn('data', result) + self.assertIn('data', result) # Test assign tag to device response = self.api.tag_assignment_config("ELEMENT_TYPE_DEVICE", new_workspace_id, diff --git a/test/unit/test_client.py b/test/unit/test_client.py index c8f10df..7d6257a 100644 --- a/test/unit/test_client.py +++ b/test/unit/test_client.py @@ -205,6 +205,8 @@ def test_make_request_good(self): self.clnt.session = Mock() self.clnt.session.return_value = True request_return_value = Mock() + request_return_value.json.return_value = [{"modelName": "vEOS"}, + {"modelName": "vEOS"}] self.clnt.session.get.return_value = request_return_value self.clnt._create_session = Mock() self.clnt.NUM_RETRY_REQUESTS = 2 @@ -283,8 +285,8 @@ def test_make_request_response_content_single_json_object(self): self.clnt.session.return_value = True response_mock = Mock() response_mock.content = b'{"data":"success"}' - response_mock.json.return_value = {"data": "success"} - response_mock.text = '{"data":"success"}' + response_mock.json.return_value = {"result": {"value": "value"}} + response_mock.text = '{"result":{"value":"value"}}' self.clnt.session.get.return_value = response_mock self.clnt._create_session = Mock() self.clnt.NUM_RETRY_REQUESTS = 2 @@ -294,7 +296,7 @@ def test_make_request_response_content_single_json_object(self): self.clnt._is_good_response = Mock(return_value='Good') self.assertIsNone(self.clnt.last_used_node) resp = self.clnt._make_request('GET', 'url', 2, {'data': 'data'}) - expected_response = {"data": "success"} + expected_response = {"result": {"value": "value"}} self.assertEqual(resp, expected_response) self.assertEqual(self.clnt.last_used_node, '1.1.1.1') From 03cc9b969df7fbc008b5d605a4e67ea7519693a4 Mon Sep 17 00:00:00 2001 From: mharista Date: Wed, 28 Sep 2022 16:12:45 -0400 Subject: [PATCH 14/19] Simplify Version Checks (#211) --- cvprac/cvp_api.py | 660 +++++++++++++++++++++------------------------- 1 file changed, 300 insertions(+), 360 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index fa23851..fa707c1 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -31,6 +31,7 @@ # ''' Class containing calls to CVP RESTful API. ''' +import operator import os import time # This import is for proper file IO handling support for both Python 2 and 3 @@ -45,6 +46,14 @@ except (AttributeError, ImportError): from urllib.parse import quote_plus as qplus +OPERATOR_DICT = { + '>': operator.gt, + '<': operator.lt, + '>=': operator.ge, + '<=': operator.le, + '==': operator.eq +} + class CvpApi(object): ''' CvpApi class contains calls to CVP RESTful API. The RESTful API @@ -85,15 +94,41 @@ def __init__(self, clnt, request_timeout=30): self.log = clnt.log self.request_timeout = request_timeout - def check_v7(self, msg): - ''' Checking the version is above CVP 2021.3.0 + def _version_compare(self, opr, version, msg): + ''' Check provided version with given operator against the current CVP + version + + Args: + opr (string): The operator. Valid operators are: + > - Greater Than + < - Less Than + >= - Greater Than or Equal To + <= - Less Than or Equal To + == - Equal To + version (float): The float API Version number to compare the + running CVP version to. ''' - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 7.0: - self.log.warning(msg) - return False + if opr not in OPERATOR_DICT: + self.log.error('%s is an invalid operation for version comparison' + % opr) + return False + + # Since CVaaS is automatically the latest version of the API, if + # operators > or >= are provided we can quickly check if we are running + # on CVaaS and return True if found. + if opr in ['>', '>='] and self.clnt.is_cvaas: + return True + + if self.clnt.apiversion is None: + self.get_cvp_info() + + # Example: if a version of 6.0 is provided with greater than or equal + # operator (>=) we are validating that the running CVP version is + # greater than or equal to API Version 6.0. + # Hence -- self.clnt.apiversion >= 6.0 + if not OPERATOR_DICT[opr](self.clnt.apiversion, version): + self.log.warning(msg) + return False return True def get_cvp_info(self): @@ -2975,21 +3010,22 @@ def get_all_tags(self, element_type='ELEMENT_TYPE_UNSPECIFIED', workspace_id='') Returns: response (dict): A dict that contains a list of key-value tags ''' - tag_url = '/api/resources/tag/v2/Tag/all' - payload = { - "partialEqFilter": [ - {"key": {"elementType": element_type, "workspaceId": workspace_id}} - ] - } + msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {}'.format(tag_url)) - return self.clnt.post(tag_url, data=payload) + if self._version_compare('>=', 6.0, msg): + tag_url = '/api/resources/tag/v2/Tag/all' + payload = { + "partialEqFilter": [ + { + "key": { + "elementType": element_type, + "workspaceId": workspace_id + } + } + ] + } + self.log.debug('v6 {}'.format(tag_url)) + return self.clnt.post(tag_url, data=payload) def get_tag_edits(self, workspace_id): ''' Show all tags edits in a workspace @@ -3003,25 +3039,21 @@ def get_tag_edits(self, workspace_id): 'elementType': 'string', 'label': 'string', 'value': 'string'}, 'remove': False}, 'time': 'rfc3339 time', 'type': 'INITIAL'}}]} ''' - tag_url = '/api/resources/tag/v2/TagConfig/all' - payload = { - "partialEqFilter": [ - { - "key": { - "workspace_id": workspace_id - } - } - ] - } + msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 ' + tag_url + ' ' + str(payload)) - return self.clnt.post(tag_url, data=payload) + if self._version_compare('>=', 6.0, msg): + tag_url = '/api/resources/tag/v2/TagConfig/all' + payload = { + "partialEqFilter": [ + { + "key": { + "workspace_id": workspace_id + } + } + ] + } + self.log.debug('v6 ' + tag_url + ' ' + str(payload)) + return self.clnt.post(tag_url, data=payload) def get_tag_assignment_edits(self, workspace_id): ''' Show all tags assignment edits in a workspace @@ -3035,25 +3067,21 @@ def get_tag_assignment_edits(self, workspace_id): 'label': 'string', 'value': 'string', 'deviceId': 'string'}, 'remove': False}, 'time': 'rfc3339', 'type': 'INITIAL'}} ''' - tag_url = '/api/resources/tag/v2/TagAssignmentConfig/all' - payload = { - "partialEqFilter": [ - { - "key": { - "workspace_id": workspace_id - } - } - ] - } + msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 ' + tag_url + ' ' + str(payload)) - return self.clnt.post(tag_url, data=payload) + if self._version_compare('>=', 6.0, msg): + tag_url = '/api/resources/tag/v2/TagAssignmentConfig/all' + payload = { + "partialEqFilter": [ + { + "key": { + "workspace_id": workspace_id + } + } + ] + } + self.log.debug('v6 ' + tag_url + ' ' + str(payload)) + return self.clnt.post(tag_url, data=payload) def tag_config(self, element_type, workspace_id, tag_label, tag_value, remove=False): ''' Create/Delete device or interface tags. @@ -3075,25 +3103,21 @@ def tag_config(self, element_type, workspace_id, tag_label, tag_value, remove=Fa 'label': 'string', 'value': 'string'}}, 'time': 'rfc3339 time'} ''' - tag_url = '/api/resources/tag/v2/TagConfig' - payload = { - "key": { - "elementType": element_type, - "workspaceId": workspace_id, - "label": tag_label, - "value": tag_value - }, - "remove": remove - } + msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {} '.format(tag_url) + str(payload)) - return self.clnt.post(tag_url, data=payload) + if self._version_compare('>=', 6.0, msg): + tag_url = '/api/resources/tag/v2/TagConfig' + payload = { + "key": { + "elementType": element_type, + "workspaceId": workspace_id, + "label": tag_label, + "value": tag_value + }, + "remove": remove + } + self.log.debug('v6 {} '.format(tag_url) + str(payload)) + return self.clnt.post(tag_url, data=payload) def tag_assignment_config(self, element_type, workspace_id, tag_label, tag_value, device_id, interface_id, remove=False): @@ -3121,27 +3145,23 @@ def tag_assignment_config(self, element_type, workspace_id, tag_label, 'remove': Boolean},'time': 'rfc3339 time'} ''' - tag_url = '/api/resources/tag/v2/TagAssignmentConfig' - payload = { - "key": { - "elementType": element_type, - "workspaceId": workspace_id, - "label": tag_label, - "value": tag_value, - "deviceId": device_id, - "interfaceId": interface_id - }, - "remove": remove - } + msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning('Tag.V2 Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {} '.format(tag_url) + str(payload)) - return self.clnt.post(tag_url, data=payload) + if self._version_compare('>=', 6.0, msg): + tag_url = '/api/resources/tag/v2/TagAssignmentConfig' + payload = { + "key": { + "elementType": element_type, + "workspaceId": workspace_id, + "label": tag_label, + "value": tag_value, + "deviceId": device_id, + "interfaceId": interface_id + }, + "remove": remove + } + self.log.debug('v6 {} '.format(tag_url) + str(payload)) + return self.clnt.post(tag_url, data=payload) def get_all_workspaces(self): ''' Get state information for all workspaces @@ -3149,17 +3169,13 @@ def get_all_workspaces(self): Returns: response (dict): A dict that contains a list of key-values for workspaces ''' - workspace_url = '/api/resources/workspace/v1/Workspace/all' - payload = {} + msg = 'Workspace Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning('Workspace Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {}'.format(workspace_url)) - return self.clnt.post(workspace_url, data=payload) + if self._version_compare('>=', 6.0, msg): + workspace_url = '/api/resources/workspace/v1/Workspace/all' + payload = {} + self.log.debug('v6 {}'.format(workspace_url)) + return self.clnt.post(workspace_url, data=payload) def get_workspace(self, workspace_id): ''' Get state information for all workspaces @@ -3167,17 +3183,13 @@ def get_workspace(self, workspace_id): Returns: response (dict): A dict that contains a list of key-values for workspaces ''' - workspace_url = '/api/resources/workspace/v1/Workspace?key.workspaceId={}'.format( - workspace_id) + msg = 'Workspace Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning('Workspace Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {}'.format(workspace_url)) - return self.clnt.get(workspace_url) + if self._version_compare('>=', 6.0, msg): + workspace_url = '/api/resources/workspace/v1/Workspace?key.workspaceId={}'.format( + workspace_id) + self.log.debug('v6 {}'.format(workspace_url)) + return self.clnt.get(workspace_url) def workspace_config(self, workspace_id, display_name, description='', request='REQUEST_UNSPECIFIED', @@ -3207,27 +3219,23 @@ def workspace_config(self, workspace_id, display_name, }, 'time': 'rfc3339 time'} ''' - workspace_url = '/api/resources/workspace/v1/WorkspaceConfig' - payload = { - "key": { - "workspaceId": workspace_id - }, - "displayName": display_name, - "description": description, - "request": request, - "requestParams": { - "requestId": request_id - } - } + msg = 'Workspace Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning('Workspace Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 ' + str(workspace_url) + ' ' + str(payload)) - return self.clnt.post(workspace_url, data=payload) + if self._version_compare('>=', 6.0, msg): + workspace_url = '/api/resources/workspace/v1/WorkspaceConfig' + payload = { + "key": { + "workspaceId": workspace_id + }, + "displayName": display_name, + "description": description, + "request": request, + "requestParams": { + "requestId": request_id + } + } + self.log.debug('v6 ' + str(workspace_url) + ' ' + str(payload)) + return self.clnt.post(workspace_url, data=payload) def workspace_build_status(self, workspace_id, build_id): ''' Verify the state of the workspace build process. @@ -3241,17 +3249,13 @@ def workspace_build_status(self, workspace_id, build_id): Ex: {'value': {'key': {'workspaceId': 'string', 'buildId': 'string'}, 'state': 'BUILD_STATE_SUCCESS', 'buildResults': {'values': ... ''' - params = 'key.workspaceId={}&key.buildId={}'.format(workspace_id, build_id) - workspace_url = '/api/resources/workspace/v1/WorkspaceBuild?' + params + msg = 'Workspace Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning('Workspace Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {}'.format(workspace_url + params)) - return self.clnt.get(workspace_url, timeout=self.request_timeout) + if self._version_compare('>=', 6.0, msg): + params = 'key.workspaceId={}&key.buildId={}'.format(workspace_id, build_id) + workspace_url = '/api/resources/workspace/v1/WorkspaceBuild?' + params + self.log.debug('v6 {}'.format(workspace_url + params)) + return self.clnt.get(workspace_url, timeout=self.request_timeout) def change_control_get_one(self, cc_id, cc_time=None): ''' Get the configuration and status of a change control using Resource APIs. @@ -3275,27 +3279,22 @@ def change_control_get_one(self, cc_id, cc_time=None): "approve":{"value":true, "time":"2021-12-13T21:11:26.788753264Z", "user":"cvpadmin"}}, "time":"2021-12-13T21:11:26.788753264Z"}% ''' - if cc_time is None: - params = 'key.id={}'.format(cc_id) - else: - params = 'key.id={}&time={}'.format(cc_id, cc_time) - cc_url = '/api/resources/changecontrol/v1/ChangeControl?' + params + msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning( - 'Change Control Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {}'.format(cc_url)) - try: - response = self.clnt.get(cc_url, timeout=self.request_timeout) - except Exception as error: - if 'resource not found' in str(error): - return None - raise error - return response + if self._version_compare('>=', 6.0, msg): + if cc_time is None: + params = 'key.id={}'.format(cc_id) + else: + params = 'key.id={}&time={}'.format(cc_id, cc_time) + cc_url = '/api/resources/changecontrol/v1/ChangeControl?' + params + self.log.debug('v6 {}'.format(cc_url)) + try: + response = self.clnt.get(cc_url, timeout=self.request_timeout) + except Exception as error: + if 'resource not found' in str(error): + return None + raise error + return response def change_control_get_all(self): ''' Get the configuration and status of all Change Controls using Resource APIs. @@ -3304,17 +3303,12 @@ def change_control_get_all(self): Returns: response (dict): A dict that contains a list of all Change Controls. ''' - cc_url = '/api/resources/changecontrol/v1/ChangeControl/all' + msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning( - 'Change Control Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {}'.format(cc_url)) - return self.clnt.get(cc_url, timeout=self.request_timeout) + if self._version_compare('>=', 6.0, msg): + cc_url = '/api/resources/changecontrol/v1/ChangeControl/all' + self.log.debug('v6 {}'.format(cc_url)) + return self.clnt.get(cc_url, timeout=self.request_timeout) def change_control_approval_get_one(self, cc_id, cc_time=None): ''' Get the state of a specific Change Control's approve config using Resource APIs. @@ -3331,28 +3325,23 @@ def change_control_approval_get_one(self, cc_id, cc_time=None): 'version': '2021-12-13T21:05:58.813750128Z'}, 'time': '2021-12-13T21:11:26.788753264Z'} ''' - if cc_time is None: - params = 'key.id={}'.format(cc_id) - else: - params = 'key.id={}&time={}'.format(cc_id, cc_time) - cc_url = '/api/resources/changecontrol/v1/ApproveConfig?' + params + msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning( - 'Change Control Resource APIs are supported from 2021.2.0 or newer.') + if self._version_compare('>=', 6.0, msg): + if cc_time is None: + params = 'key.id={}'.format(cc_id) + else: + params = 'key.id={}&time={}'.format(cc_id, cc_time) + cc_url = '/api/resources/changecontrol/v1/ApproveConfig?' + params + cc_status = self.change_control_get_one(cc_id) + if cc_status is None: return None - cc_status = self.change_control_get_one(cc_id) - if cc_status is None: - return None - if 'value' in cc_status and 'approve' not in cc_status['value']: - self.log.warning("The change has not been approved yet." - " A change has to be approved at least once for the 'approve'" - " state to be populated.") - return None - return self.clnt.get(cc_url, timeout=self.request_timeout) + if 'value' in cc_status and 'approve' not in cc_status['value']: + self.log.warning("The change has not been approved yet." + " A change has to be approved at least once for the 'approve'" + " state to be populated.") + return None + return self.clnt.get(cc_url, timeout=self.request_timeout) def change_control_approval_get_all(self): ''' Get state information for all Change Control Approvals using Resource APIs. @@ -3361,18 +3350,12 @@ def change_control_approval_get_all(self): Returns: response (dict): A dict that contains a list of all Change Control Approval Configs. ''' - cc_url = '/api/resources/changecontrol/v1/ApproveConfig/all' + msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning( - 'Change Control Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {}'.format(cc_url)) - return self.clnt.get(cc_url, timeout=self.request_timeout) + if self._version_compare('>=', 6.0, msg): + cc_url = '/api/resources/changecontrol/v1/ApproveConfig/all' + self.log.debug('v6 {}'.format(cc_url)) + return self.clnt.get(cc_url, timeout=self.request_timeout) def change_control_approve(self, cc_id, notes="", approve=True): ''' Approve/Unapprove a change control using Resource APIs. @@ -3415,18 +3398,13 @@ def change_control_delete(self, cc_id): Args: cc_id (str): The ID of the change control. ''' - params = 'key.id={}'.format(cc_id) - cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig?' + params + msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning( - 'Change Control Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 {}'.format(cc_url)) - return self.clnt.delete(cc_url, timeout=self.request_timeout) + if self._version_compare('>=', 6.0, msg): + params = 'key.id={}'.format(cc_id) + cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig?' + params + self.log.debug('v6 {}'.format(cc_url)) + return self.clnt.delete(cc_url, timeout=self.request_timeout) def change_control_create_with_custom_stages(self, custom_cc=None): ''' Create a Change Control with custom stage hierarchy using Resource APIs. @@ -3554,18 +3532,13 @@ def change_control_create_with_custom_stages(self, custom_cc=None): Ex: {'value': {'key': {'id':cc_id, 'time': '...'} ''' - payload = custom_cc - cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning( - 'Change Control Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload)) - return self.clnt.post(cc_url, data=payload) + if self._version_compare('>=', 6.0, msg): + payload = custom_cc + cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload)) + return self.clnt.post(cc_url, data=payload) def change_control_create_for_tasks(self, cc_id, name, tasks, series=True): ''' Create a simple Change Control for tasks using Resource APIs. @@ -3620,28 +3593,23 @@ def change_control_create_for_tasks(self, cc_id, name, tasks, series=True): }, 'name': stage_id, } - payload = { - 'key': { - 'id': cc_id - }, - 'change': { - 'name': name, - 'rootStageId': 'root', - 'notes': 'randomString', - 'stages': stages - } - } - cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning( - 'Change Control Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload)) - return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout) + if self._version_compare('>=', 6.0, msg): + payload = { + 'key': { + 'id': cc_id + }, + 'change': { + 'name': name, + 'rootStageId': 'root', + 'notes': 'randomString', + 'stages': stages + } + } + cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload)) + return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout) def change_control_start(self, cc_id, notes=""): ''' Start a Change Control using Resource APIs. @@ -3654,26 +3622,21 @@ def change_control_start(self, cc_id, notes=""): Ex: {"value":{"key":{"id":cc_id}, "start":{"value":true, "notes":"note"}}, "time":"2021-12-14T21:02:21.830306071Z"} ''' - payload = { - "key": { - "id": cc_id - }, - "start": { - "value": True, - "notes": notes - } - } - cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning( - 'Change Control Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload)) - return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout) + if self._version_compare('>=', 6.0, msg): + payload = { + "key": { + "id": cc_id + }, + "start": { + "value": True, + "notes": notes + } + } + cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload)) + return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout) def change_control_stop(self, cc_id, notes=""): ''' Stop a Change Control using Resource APIs. @@ -3687,26 +3650,21 @@ def change_control_stop(self, cc_id, notes=""): Ex: {"value":{"key":{"id":cc_id}, "start":{"value":false, "notes":"note"}}, "time":"2021-12-14T21:02:21.830306071Z"} ''' - payload = { - "key": { - "id": cc_id - }, - "start": { - "value": False, - "notes": notes - } - } - cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 6.0: - self.log.warning( - 'Change Control Resource APIs are supported from 2021.2.0 or newer.') - return None - self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload)) - return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout) + if self._version_compare('>=', 6.0, msg): + payload = { + "key": { + "id": cc_id + }, + "start": { + "value": False, + "notes": notes + } + } + cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload)) + return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout) def change_control_schedule(self, cc_id, schedule_time, notes=""): ''' Schedule a Change Control using Resource APIs. @@ -3723,26 +3681,21 @@ def change_control_schedule(self, cc_id, schedule_time, notes=""): "notes":"CC schedule via curl"}}, "time":"2021-12-23T02:06:18.739965204Z"} ''' - payload = { - "key": { - "id": cc_id - }, - "schedule": { - "value": schedule_time, - "notes": notes - } - } - cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + msg = 'Change Control Scheduling via Resource APIs are supported from 2022.1.0 or newer.' # For on-prem check the version as it is only supported from 2022.1.0+ - if not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 8.0: - self.log.warning( - 'Change Control Scheduling via Resource APIs are supported from 2022.1.0 or newer.') - return None - self.log.debug('v8 ' + str(cc_url) + ' ' + str(payload)) - return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout) + if self._version_compare('>=', 8.0, msg): + payload = { + "key": { + "id": cc_id + }, + "schedule": { + "value": schedule_time, + "notes": notes + } + } + cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' + self.log.debug('v8 ' + str(cc_url) + ' ' + str(payload)) + return self.clnt.post(cc_url, data=payload, timeout=self.request_timeout) def device_decommissioning(self, device_id, request_id): ''' Decommission a device using Resource APIs. @@ -3759,25 +3712,22 @@ def device_decommissioning(self, device_id, request_id): ''' device_info = self.get_device_by_serial(device_id) if device_info is not None and 'serialNumber' in device_info: - payload = { - "key": { - "request_id": request_id - }, - "device_id": device_id - } - url = '/api/resources/inventory/v1/DeviceDecommissioningConfig' + 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 not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 7.0: - self.log.warning( - 'Decommissioning via Resource APIs are supported from 2021.3.0 or newer.') - return None - self.log.debug('v7 ' + str(url) + ' ' + str(payload)) - return self.clnt.post(url, data=payload, timeout=self.request_timeout) + if self._version_compare('>=', 7.0, msg): + payload = { + "key": { + "request_id": request_id + }, + "device_id": device_id + } + url = '/api/resources/inventory/v1/DeviceDecommissioningConfig' + self.log.debug('v7 ' + str(url) + ' ' + str(payload)) + return self.clnt.post(url, data=payload, timeout=self.request_timeout) else: - self.log.warning('Device with %s serial number does not exist (or is not registered) to decommission' % device_id) + self.log.warning( + 'Device with %s serial number does not exist (or is not registered) to decommission' + % device_id) return None def device_decommissioning_status_get_one(self, request_id): @@ -3792,18 +3742,13 @@ def device_decommissioning_status_get_one(self, request_id): "statusMessage":"Disabled TerminAttr, waiting for device to be marked inactive"}, "time":"2022-02-04T19:41:46.376310308Z","type":"INITIAL"}} ''' - params = 'key.requestId={}'.format(request_id) - url = '/api/resources/inventory/v1/DeviceDecommissioning?' + params + 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 not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 7.0: - self.log.warning( - 'Decommissioning via Resource APIs are supported from 2021.3.0 or newer.') - return None - self.log.debug('v7 ' + str(url)) - return self.clnt.get(url, timeout=self.request_timeout) + if self._version_compare('>=', 7.0, msg): + params = 'key.requestId={}'.format(request_id) + url = '/api/resources/inventory/v1/DeviceDecommissioning?' + params + self.log.debug('v7 ' + str(url)) + return self.clnt.get(url, timeout=self.request_timeout) def device_decommissioning_status_get_all(self, status="DECOMMISSIONING_STATUS_UNSPECIFIED"): ''' Get the decommissioning status of all devices using Resource APIs. @@ -3822,24 +3767,19 @@ def device_decommissioning_status_get_all(self, status="DECOMMISSIONING_STATUS_U "statusMessage":"Disabled TerminAttr, waiting for device to be marked inactive"}, "time":"2022-02-04T19:41:46.376310308Z","type":"INITIAL"}} ''' - payload = { - "partialEqFilter": [ - { - "status": status, - } - ] - } - url = '/api/resources/inventory/v1/DeviceDecommissioning/all' + 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 not self.clnt.is_cvaas: - if self.clnt.apiversion is None: - self.get_cvp_info() - if self.clnt.apiversion < 7.0: - self.log.warning( - 'Decommissioning via Resource APIs are supported from 2021.3.0 or newer.') - return None - self.log.debug('v7 ' + str(url)) - return self.clnt.post(url, data=payload, timeout=self.request_timeout) + if self._version_compare('>=', 7.0, msg): + payload = { + "partialEqFilter": [ + { + "status": status, + } + ] + } + url = '/api/resources/inventory/v1/DeviceDecommissioning/all' + self.log.debug('v7 ' + str(url)) + return self.clnt.post(url, data=payload, timeout=self.request_timeout) def get_roles(self): ''' Get all the user roles in CloudVision. @@ -3862,7 +3802,7 @@ def svc_account_token_get_all(self): 'time': '2022-05-03T15:38:53.725014447Z', 'type': 'INITIAL'}, ...] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self.check_v7(msg): + if self._version_compare('>=', 7.0, msg): url = '/api/v3/services/arista.serviceaccount.v1.TokenService/GetAll' self.log.debug('v7 {}'.format(url)) return self.clnt.post(url) @@ -3878,7 +3818,7 @@ def svc_account_token_get_one(self, token_id): 'time': '2022-05-03T15:38:53.725014447Z', 'type': 'INITIAL'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self.check_v7(msg): + if self._version_compare('>=', 7.0, msg): payload = {"key": {"id": token_id}} url = '/api/v3/services/arista.serviceaccount.v1.TokenService/GetOne' self.log.debug('v7 {} {}'.format(url, payload)) @@ -3895,7 +3835,7 @@ def svc_account_token_delete(self, token_id): 'time': '2022-07-26T15:29:03.687167871Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self.check_v7(msg): + if self._version_compare('>=', 7.0, msg): payload = {"key": {"id": token_id}} url = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Delete' self.log.debug('v7 {} {}'.format(url, payload)) @@ -3920,7 +3860,7 @@ def svc_account_token_set(self, username, duration, description): 'user': username, 'valid_for': duration}} msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self.check_v7(msg): + if self._version_compare('>=', 7.0, msg): url = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Set' self.log.debug('v7 {} {}'.format(url, payload)) return self.clnt.post(url, data=payload) @@ -3936,7 +3876,7 @@ def svc_account_get_all(self): ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self.check_v7(msg): + if self._version_compare('>=', 7.0, msg): url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/GetAll' self.log.debug('v7 {} '.format(url)) return self.clnt.post(url) @@ -3953,7 +3893,7 @@ def svc_account_get_one(self, username): 'time': '2022-02-10T04:28:14.251684869Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self.check_v7(msg): + if self._version_compare('>=', 7.0, msg): payload = {"key": {"name": username}} url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/GetOne' self.log.debug('v7 {} {}'.format(url, payload)) @@ -3982,7 +3922,7 @@ def svc_account_set(self, username, description, roles, status): 'time': '2022-07-26T18:19:55.392173445Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self.check_v7(msg): + if self._version_compare('>=', 7.0, msg): role_ids = [] all_roles = self.get_roles() for role in all_roles['roles']: @@ -4011,7 +3951,7 @@ def svc_account_delete(self, username): 'time': '2022-07-26T18:26:53.637425846Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self.check_v7(msg): + if self._version_compare('>=', 7.0, msg): payload = {"key": {"name": username}} url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/Delete' self.log.debug('v7 {} {}'.format(url, payload)) From 626b4e62795828be0eba24b6a33e58c91f520e32 Mon Sep 17 00:00:00 2001 From: Anton Sibiriakov Date: Thu, 29 Sep 2022 11:05:29 -0700 Subject: [PATCH 15/19] Example: add configlet search example (#214) --- .../config_search.py | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/labs/lab03-configlet-management/config_search.py diff --git a/docs/labs/lab03-configlet-management/config_search.py b/docs/labs/lab03-configlet-management/config_search.py new file mode 100644 index 0000000..4c3ad27 --- /dev/null +++ b/docs/labs/lab03-configlet-management/config_search.py @@ -0,0 +1,54 @@ +# Copyright (c) 2022 Arista Networks, Inc. +# Use of this source code is governed by the Apache License 2.0 +# that can be found in the COPYING file. + +from cvprac.cvp_client import CvpClient +import ssl +ssl._create_default_https_context = ssl._create_unverified_context +import requests.packages.urllib3 +requests.packages.urllib3.disable_warnings() + +# Create connection to CloudVision +clnt = CvpClient() +clnt.connect(nodes=['cvp1'], username="username",password="password") + +def main(): + + print('Retrieving configlets ...') + + inventory = clnt.api.get_inventory() + data = clnt.api.get_configlets_and_mappers()['data'] + print(data) + + print('Number of configlets:', len(data['configlets'])) + + searchAgain = True + while searchAgain: + try: + search = input( "\nEnter Config Line: " ) + print(f"\n\n\'{search}\' has been found in following configlets:\n\n") + print(f"{'Hostname':<30}{'Serial number':<50}{'MAC address':<30}{'Configlets':<40}") + print("=" * 150) + for i in inventory: + device = i['hostname'] + device_sn = i['serialNumber'] + device_mac = i['systemMacAddress'] + configlet_list = [] + for c in data['configlets']: + for g in data['generatedConfigletMappers']: + if device_mac == g['netElementId'] and c['key'] == g['configletBuilderId'] and search in c['config']: + configlet_list.append(c['name']) + for k in data['configletMappers']: + if device_mac == k['objectId'] and c['key'] == k['configletId'] and search in c['config']: + configlet_list.append(c['name']) + configlet_list_final = ",".join(configlet_list) + if len(configlet_list) > 0: + print(f"{device:<30}{device_sn:<50}{device_mac:<30}{configlet_list_final:<30}") + + except KeyboardInterrupt: + print('\nExiting... \n') + return + +if __name__ == '__main__': + main() + From 1a5e1ed47f1685ac43eacd5a8272632a777d712f Mon Sep 17 00:00:00 2001 From: MattH Date: Fri, 30 Sep 2022 16:19:45 -0400 Subject: [PATCH 16/19] Fix: Limit requests package to <=2.27.1 for unittests --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5a687fa..5a81d02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests>=1.0.0 +requests>=1.0.0, <=2.27.1 From 8507ebd5fb2a8d826bc6d0cf80992eb31530adc7 Mon Sep 17 00:00:00 2001 From: mharista Date: Wed, 5 Oct 2022 11:29:09 -0400 Subject: [PATCH 17/19] Refactor: allow system tests to use version_compare for running service account tests (#218) --- cvprac/cvp_api.py | 62 +++---- test/system/test_cvp_client_api.py | 249 ++++++++++++++++------------- 2 files changed, 166 insertions(+), 145 deletions(-) diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index fa707c1..22b460a 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -94,7 +94,7 @@ def __init__(self, clnt, request_timeout=30): self.log = clnt.log self.request_timeout = request_timeout - def _version_compare(self, opr, version, msg): + def cvp_version_compare(self, opr, version, msg): ''' Check provided version with given operator against the current CVP version @@ -3012,7 +3012,7 @@ def get_all_tags(self, element_type='ELEMENT_TYPE_UNSPECIFIED', workspace_id='') ''' msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): tag_url = '/api/resources/tag/v2/Tag/all' payload = { "partialEqFilter": [ @@ -3041,7 +3041,7 @@ def get_tag_edits(self, workspace_id): ''' msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): tag_url = '/api/resources/tag/v2/TagConfig/all' payload = { "partialEqFilter": [ @@ -3069,7 +3069,7 @@ def get_tag_assignment_edits(self, workspace_id): ''' msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): tag_url = '/api/resources/tag/v2/TagAssignmentConfig/all' payload = { "partialEqFilter": [ @@ -3105,7 +3105,7 @@ def tag_config(self, element_type, workspace_id, tag_label, tag_value, remove=Fa ''' msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): tag_url = '/api/resources/tag/v2/TagConfig' payload = { "key": { @@ -3147,7 +3147,7 @@ def tag_assignment_config(self, element_type, workspace_id, tag_label, ''' msg = 'Tag.V2 Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): tag_url = '/api/resources/tag/v2/TagAssignmentConfig' payload = { "key": { @@ -3171,7 +3171,7 @@ def get_all_workspaces(self): ''' msg = 'Workspace Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): workspace_url = '/api/resources/workspace/v1/Workspace/all' payload = {} self.log.debug('v6 {}'.format(workspace_url)) @@ -3185,7 +3185,7 @@ def get_workspace(self, workspace_id): ''' msg = 'Workspace Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): workspace_url = '/api/resources/workspace/v1/Workspace?key.workspaceId={}'.format( workspace_id) self.log.debug('v6 {}'.format(workspace_url)) @@ -3221,7 +3221,7 @@ def workspace_config(self, workspace_id, display_name, ''' msg = 'Workspace Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): workspace_url = '/api/resources/workspace/v1/WorkspaceConfig' payload = { "key": { @@ -3251,7 +3251,7 @@ def workspace_build_status(self, workspace_id, build_id): ''' msg = 'Workspace Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): params = 'key.workspaceId={}&key.buildId={}'.format(workspace_id, build_id) workspace_url = '/api/resources/workspace/v1/WorkspaceBuild?' + params self.log.debug('v6 {}'.format(workspace_url + params)) @@ -3281,7 +3281,7 @@ def change_control_get_one(self, cc_id, cc_time=None): ''' msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): if cc_time is None: params = 'key.id={}'.format(cc_id) else: @@ -3305,7 +3305,7 @@ def change_control_get_all(self): ''' msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): cc_url = '/api/resources/changecontrol/v1/ChangeControl/all' self.log.debug('v6 {}'.format(cc_url)) return self.clnt.get(cc_url, timeout=self.request_timeout) @@ -3327,7 +3327,7 @@ def change_control_approval_get_one(self, cc_id, cc_time=None): ''' msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): if cc_time is None: params = 'key.id={}'.format(cc_id) else: @@ -3352,7 +3352,7 @@ def change_control_approval_get_all(self): ''' msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): cc_url = '/api/resources/changecontrol/v1/ApproveConfig/all' self.log.debug('v6 {}'.format(cc_url)) return self.clnt.get(cc_url, timeout=self.request_timeout) @@ -3400,7 +3400,7 @@ def change_control_delete(self, cc_id): ''' msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): params = 'key.id={}'.format(cc_id) cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig?' + params self.log.debug('v6 {}'.format(cc_url)) @@ -3534,7 +3534,7 @@ def change_control_create_with_custom_stages(self, custom_cc=None): ''' msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): payload = custom_cc cc_url = '/api/resources/changecontrol/v1/ChangeControlConfig' self.log.debug('v6 ' + str(cc_url) + ' ' + str(payload)) @@ -3595,7 +3595,7 @@ def change_control_create_for_tasks(self, cc_id, name, tasks, series=True): } msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): payload = { 'key': { 'id': cc_id @@ -3624,7 +3624,7 @@ def change_control_start(self, cc_id, notes=""): ''' msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): payload = { "key": { "id": cc_id @@ -3652,7 +3652,7 @@ def change_control_stop(self, cc_id, notes=""): ''' msg = 'Change Control Resource APIs are supported from 2021.2.0 or newer.' # For on-prem check the version as it is only supported from 2021.2.0+ - if self._version_compare('>=', 6.0, msg): + if self.cvp_version_compare('>=', 6.0, msg): payload = { "key": { "id": cc_id @@ -3683,7 +3683,7 @@ def change_control_schedule(self, cc_id, schedule_time, notes=""): ''' msg = 'Change Control Scheduling via Resource APIs are supported from 2022.1.0 or newer.' # For on-prem check the version as it is only supported from 2022.1.0+ - if self._version_compare('>=', 8.0, msg): + if self.cvp_version_compare('>=', 8.0, msg): payload = { "key": { "id": cc_id @@ -3714,7 +3714,7 @@ def device_decommissioning(self, device_id, request_id): if device_info is not None and 'serialNumber' in device_info: 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._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): payload = { "key": { "request_id": request_id @@ -3744,7 +3744,7 @@ def device_decommissioning_status_get_one(self, request_id): ''' 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._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): params = 'key.requestId={}'.format(request_id) url = '/api/resources/inventory/v1/DeviceDecommissioning?' + params self.log.debug('v7 ' + str(url)) @@ -3769,7 +3769,7 @@ def device_decommissioning_status_get_all(self, status="DECOMMISSIONING_STATUS_U ''' 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._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): payload = { "partialEqFilter": [ { @@ -3802,7 +3802,7 @@ def svc_account_token_get_all(self): 'time': '2022-05-03T15:38:53.725014447Z', 'type': 'INITIAL'}, ...] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): url = '/api/v3/services/arista.serviceaccount.v1.TokenService/GetAll' self.log.debug('v7 {}'.format(url)) return self.clnt.post(url) @@ -3818,7 +3818,7 @@ def svc_account_token_get_one(self, token_id): 'time': '2022-05-03T15:38:53.725014447Z', 'type': 'INITIAL'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): payload = {"key": {"id": token_id}} url = '/api/v3/services/arista.serviceaccount.v1.TokenService/GetOne' self.log.debug('v7 {} {}'.format(url, payload)) @@ -3835,7 +3835,7 @@ def svc_account_token_delete(self, token_id): 'time': '2022-07-26T15:29:03.687167871Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): payload = {"key": {"id": token_id}} url = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Delete' self.log.debug('v7 {} {}'.format(url, payload)) @@ -3860,7 +3860,7 @@ def svc_account_token_set(self, username, duration, description): 'user': username, 'valid_for': duration}} msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): url = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Set' self.log.debug('v7 {} {}'.format(url, payload)) return self.clnt.post(url, data=payload) @@ -3876,7 +3876,7 @@ def svc_account_get_all(self): ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/GetAll' self.log.debug('v7 {} '.format(url)) return self.clnt.post(url) @@ -3893,7 +3893,7 @@ def svc_account_get_one(self, username): 'time': '2022-02-10T04:28:14.251684869Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): payload = {"key": {"name": username}} url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/GetOne' self.log.debug('v7 {} {}'.format(url, payload)) @@ -3922,7 +3922,7 @@ def svc_account_set(self, username, description, roles, status): 'time': '2022-07-26T18:19:55.392173445Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): role_ids = [] all_roles = self.get_roles() for role in all_roles['roles']: @@ -3951,7 +3951,7 @@ def svc_account_delete(self, username): 'time': '2022-07-26T18:26:53.637425846Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' - if self._version_compare('>=', 7.0, msg): + if self.cvp_version_compare('>=', 7.0, msg): payload = {"key": {"name": username}} url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/Delete' self.log.debug('v7 {} {}'.format(url, payload)) diff --git a/test/system/test_cvp_client_api.py b/test/system/test_cvp_client_api.py index 5499c55..3ff431e 100644 --- a/test/system/test_cvp_client_api.py +++ b/test/system/test_cvp_client_api.py @@ -225,80 +225,85 @@ def test_api_svc_account_operations(self): ''' Verify svc_account_get_all, svc_account_get_one, svc_account_set, svc_account_delete ''' - result = self.api.svc_account_get_all() - start_total = 0 - if result is not None: - if 'data' in result: - start_total = len(result['data']) - else: - start_total = len(result) - - username = "cvpractest" - description = "cvprac test" - # TODO add custom roles after role creation APIs are added - roles = ["network-admin", "network-operator"] - status = 1 # enabled - # Test get service account - try: - result = self.api.svc_account_get_one(username) - self.assertIsNotNone(result) - self.assertIn('value', result[0]) - self.assertIn('key', result[0]['value']) - self.assertIn('name', result[0]['value']['key']) - self.assertEqual(result[0]['value']['key']['name'], username) - initial_acc_status = result[0]['value']['status'] - initial_groups = result[0]['value']['groups']['values'] - except CvpRequestError: - # Test create service account - result = self.api.svc_account_set(username, description, roles, status) + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.api.cvp_version_compare('>=', 7.0, msg): + result = self.api.svc_account_get_all() + start_total = 0 + if result is not None: + if 'data' in result: + start_total = len(result['data']) + else: + start_total = len(result) + + username = "cvpractest" + description = "cvprac test" + # TODO add custom roles after role creation APIs are added + roles = ["network-admin", "network-operator"] + status = 1 # enabled + # Test get service account + try: + result = self.api.svc_account_get_one(username) + self.assertIsNotNone(result) + self.assertIn('value', result[0]) + self.assertIn('key', result[0]['value']) + self.assertIn('name', result[0]['value']['key']) + self.assertEqual(result[0]['value']['key']['name'], username) + initial_acc_status = result[0]['value']['status'] + initial_groups = result[0]['value']['groups']['values'] + except CvpRequestError: + # Test create service account + result = self.api.svc_account_set(username, description, roles, status) + self.assertIsNotNone(result) + self.assertIn('value', result[0]) + self.assertIn('key', result[0]['value']) + self.assertIn('name', result[0]['value']['key']) + self.assertEqual(result[0]['value']['key']['name'], username) + self.assertEqual(result[0]['value']['status'], 'ACCOUNT_STATUS_ENABLED') + self.assertEqual(result[0]['value']['description'], description) + self.assertEqual(result[0]['value']['groups']['values'], roles) + initial_acc_status = result[0]['value']['status'] + initial_groups = result[0]['value']['groups']['values'] + + update_acc_status = 'ACCOUNT_STATUS_ENABLED' + if initial_acc_status == 'ACCOUNT_STATUS_ENABLED': + update_acc_status = 'ACCOUNT_STATUS_DISABLED' + + update_groups = ["network-admin", "network-operator"] + if initial_groups == ["network-admin", "network-operator"]: + update_groups = ["network-operator"] + # Test update service account + result = self.api.svc_account_set(username, description, update_groups, + update_acc_status) self.assertIsNotNone(result) - self.assertIn('value', result[0]) - self.assertIn('key', result[0]['value']) - self.assertIn('name', result[0]['value']['key']) - self.assertEqual(result[0]['value']['key']['name'], username) - self.assertEqual(result[0]['value']['status'], 'ACCOUNT_STATUS_ENABLED') - self.assertEqual(result[0]['value']['description'], description) - self.assertEqual(result[0]['value']['groups']['values'], roles) - initial_acc_status = result[0]['value']['status'] - initial_groups = result[0]['value']['groups']['values'] - - update_acc_status = 'ACCOUNT_STATUS_ENABLED' - if initial_acc_status == 'ACCOUNT_STATUS_ENABLED': - update_acc_status = 'ACCOUNT_STATUS_DISABLED' - - update_groups = ["network-admin", "network-operator"] - if initial_groups == ["network-admin", "network-operator"]: - update_groups = ["network-operator"] - # Test update service account - result = self.api.svc_account_set(username, description, update_groups, - update_acc_status) - self.assertIsNotNone(result) - # Test Get all service account with new account - result = self.api.svc_account_get_all() - self.assertIsNotNone(result) - self.assertEqual(len(result), start_total + 1) + # Test Get all service account with new account + result = self.api.svc_account_get_all() + self.assertIsNotNone(result) + self.assertEqual(len(result), start_total + 1) - # Test delete service account - result = self.api.svc_account_delete(username) - self.assertIsNotNone(result) - self.assertIn('key', result[0]) - self.assertIn('name', result[0]['key']) - self.assertIn('time', result[0]) + # Test delete service account + result = self.api.svc_account_delete(username) + self.assertIsNotNone(result) + self.assertIn('key', result[0]) + self.assertIn('name', result[0]['key']) + self.assertIn('time', result[0]) - # Verify the service account was successfully deleted and doesn't exist - with self.assertRaises(CvpRequestError): - self.api.svc_account_get_one(username) + # Verify the service account was successfully deleted and doesn't exist + with self.assertRaises(CvpRequestError): + self.api.svc_account_get_one(username) - # Test Get All service accounts final - result = self.api.svc_account_get_all() - if result is not None: - if 'data' in result: - self.assertEqual(len(result['data']), start_total) + # Test Get All service accounts final + result = self.api.svc_account_get_all() + if result is not None: + if 'data' in result: + self.assertEqual(len(result['data']), start_total) + else: + self.assertEqual(len(result), start_total) else: - self.assertEqual(len(result), start_total) + self.assertEqual(0, start_total) else: - self.assertEqual(0, start_total) + pprint(f'SKIPPING TEST (svc_account) FOR API - {self.clnt.apiversion}') + time.sleep(1) def test_api_svc_account_token_operations(self): ''' Verify svc_account_set, svc_account_token_get_all, svc_account_token_set, @@ -306,58 +311,74 @@ def test_api_svc_account_token_operations(self): ''' # Test creating tokens # Create a few service accounts and several tokens for each - result = self.api.svc_account_set("cvprac1", "test", ["network-admin"], 1) - self.assertIsNotNone(result) - self.assertIn('name', result[0]['value']['key']) - result = self.api.svc_account_set("cvprac2", "test", ["network-admin"], 1) - self.assertIsNotNone(result) - self.assertIn('name', result[0]['value']['key']) - result = self.api.svc_account_token_set("cvprac1", "10s", "test1") - self.assertIsNotNone(result) - self.assertIn('id', result[0]['value']['key']) - result = self.api.svc_account_token_set("cvprac1", "5s", "test1") - self.assertIsNotNone(result) - self.assertIn('id', result[0]['value']['key']) - result = self.api.svc_account_token_set("cvprac1", "1600s", "test1") - self.assertIsNotNone(result) - self.assertIn('id', result[0]['value']['key']) - result = self.api.svc_account_token_set("cvprac2", "10s", "test2") - self.assertIsNotNone(result) - self.assertIn('id', result[0]['value']['key']) - result = self.api.svc_account_token_set("cvprac2", "5s", "test2") - self.assertIsNotNone(result) - self.assertIn('id', result[0]['value']['key']) - result = self.api.svc_account_token_set("cvprac2", "1600s", "test2") - self.assertIsNotNone(result) - self.assertIn('id', result[0]['value']['key']) - result = self.api.svc_account_token_set("cvprac2", "3600s", "test2") - self.assertIsNotNone(result) - self.assertIn('id', result[0]['value']['key']) - token_id = result[0]['value']['key']['id'] + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.api.cvp_version_compare('>=', 7.0, msg): + result = self.api.svc_account_set("cvprac1", "test", ["network-admin"], 1) + self.assertIsNotNone(result) + self.assertIn('name', result[0]['value']['key']) + result = self.api.svc_account_set("cvprac2", "test", ["network-admin"], 1) + self.assertIsNotNone(result) + self.assertIn('name', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac1", "10s", "test1") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac1", "5s", "test1") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac1", "1600s", "test1") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac2", "10s", "test2") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac2", "5s", "test2") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac2", "1600s", "test2") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + result = self.api.svc_account_token_set("cvprac2", "3600s", "test2") + self.assertIsNotNone(result) + self.assertIn('id', result[0]['value']['key']) + token_id = result[0]['value']['key']['id'] - # Test Get All service account tokens + # Test Get All service account tokens - result = self.api.svc_account_token_get_all() - self.assertIsNotNone(result) - start_total_tok = len(result) + result = self.api.svc_account_token_get_all() + self.assertIsNotNone(result) + start_total_tok = len(result) - # Test delete a service account token - result = self.api.svc_account_token_delete(token_id) - self.assertIsNotNone(result) - self.assertIn('id', result[0]['key']) - self.assertIn('time', result[0]) - total_tok_post_del_one = self.api.svc_account_token_get_all() - self.assertEqual(start_total_tok - 1, len(total_tok_post_del_one)) + # Test delete a service account token + result = self.api.svc_account_token_delete(token_id) + self.assertIsNotNone(result) + self.assertIn('id', result[0]['key']) + self.assertIn('time', result[0]) + total_tok_post_del_one = self.api.svc_account_token_get_all() + self.assertEqual(start_total_tok - 1, len(total_tok_post_del_one)) + + # Test delete all expired service account tokens + time.sleep(11) # Sleep for 11 seconds so that few of the tokens can expire + result = self.api.svc_account_delete_expired_tokens() + self.assertIsNotNone(result) + result = self.api.svc_account_token_get_all() + self.assertIsNotNone(result) + end_total_tok = len(result) + self.assertEqual(end_total_tok, start_total_tok - 5) - # Test delete all expired service account tokens + # Delete services accounts created + result = self.api.svc_account_delete("cvprac1") + self.assertIsNotNone(result) + result = self.api.svc_account_delete("cvprac2") + self.assertIsNotNone(result) - time.sleep(11) # Sleep for 11 seconds so that few of the tokens can expire - result = self.api.svc_account_delete_expired_tokens() - self.assertIsNotNone(result) - result = self.api.svc_account_token_get_all() - self.assertIsNotNone(result) - end_total_tok = len(result) - self.assertEqual(end_total_tok, start_total_tok - 5) + # Verify the service account was successfully deleted and doesn't exist + with self.assertRaises(CvpRequestError): + self.api.svc_account_get_one("cvprac1") + with self.assertRaises(CvpRequestError): + self.api.svc_account_get_one("cvprac2") + else: + pprint(f'SKIPPING TEST (svc_account_token) FOR API - {self.clnt.apiversion}') + time.sleep(1) def test_api_check_compliance(self): ''' Verify check_compliance From 833e4c39ba6875dbf12840a583620684a2280a8c Mon Sep 17 00:00:00 2001 From: MattH Date: Fri, 7 Oct 2022 16:14:39 -0400 Subject: [PATCH 18/19] Doc: Add release notes for version 1.2.2 --- docs/release-notes-1.2.2.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 docs/release-notes-1.2.2.rst diff --git a/docs/release-notes-1.2.2.rst b/docs/release-notes-1.2.2.rst new file mode 100644 index 0000000..84b8e39 --- /dev/null +++ b/docs/release-notes-1.2.2.rst @@ -0,0 +1,26 @@ +###### +v1.2.2 +###### + +2022-10-6 + +New Modules +^^^^^^^^^^^ + +* Add function for getUsers API. (`203 `_) [`mharista `_] +* Added service account Resource APIs. (`208 `_) [`noredistribution `_] + +Enhancements +^^^^^^^^^^^^ + +* Updated and added examples. (`205 `_) [`noredistribution `_] +* Update documentation for PR semantics. (`206 `_) [`tgodaA `_] +* Update development tools. (`209 `_) [`mharista `_] +* Update system tests for CVP 2022.2.0 support. (`2fcf3d2 `_) [`mharista `_] +* Simplified CVP version handling. (`211 `_) [`mharista `_] + +Fixed +^^^^^ + +* Raise error for JSON decoding when incomplete block is found. (`202 `_) [`mharista `_] +* Fixed issue with "data" key for Resource API GetAll calls that return a single object. (`215 `_) [`mharista `_] From 04c1f325d1dce12e4ee92a1496a08d8a67fa5781 Mon Sep 17 00:00:00 2001 From: MattH Date: Fri, 7 Oct 2022 16:15:59 -0400 Subject: [PATCH 19/19] Bump: versions for release 1.2.2 --- VERSION | 2 +- cvprac/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 6563189..23aa839 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -develop +1.2.2 diff --git a/cvprac/__init__.py b/cvprac/__init__.py index 1a0108c..939533d 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.2.2' __author__ = 'Arista Networks, Inc.'