diff --git a/.arista/secret_allowlist.yaml b/.arista/secret_allowlist.yaml new file mode 100644 index 0000000..ccfd00a --- /dev/null +++ b/.arista/secret_allowlist.yaml @@ -0,0 +1,6 @@ +version: v1.0 +allowed_secrets: +- filepath_literal: "docs/labs/lab07-aaa/svc_account_misc.py" + secret_literal: "9bfb39ff892c81d6ac9f25ff95d0389719595feb" + category: FALSE_POSITIVE + reason: Example token in documentation example file diff --git a/.gitignore b/.gitignore index 809ce26..898286c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,6 @@ docs/_build/ docs/_modules/modules_by_category.rst docs/_modules/list_of_*.rst docs/_modules/*_module.rst + +# .DS_Store +.DS_Store diff --git a/MANIFEST.in b/MANIFEST.in index 1942977..c6af7e8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -20,3 +20,4 @@ exclude Jenkinsfile exclude pre-commit.sh exclude report exclude htmlcov +recursive-exclude .arista *.yaml diff --git a/README.md b/README.md index d74808e..4358266 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,10 @@ using the API methods. ### Requirements -- Python 2.7 or later +- Python 3.7 or later - Python logging module -- Python requests module version 1.0.0 or later +- Python requests module with socks version 2.27.0 or later +- Python packaging module version 23.2 or later ## Installation diff --git a/VERSION b/VERSION index 88c5fb8..347f583 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 +1.4.1 diff --git a/cvprac/__init__.py b/cvprac/__init__.py index 67d57ff..870ed6f 100644 --- a/cvprac/__init__.py +++ b/cvprac/__init__.py @@ -32,5 +32,5 @@ ''' RESTful API Client class for Cloudvision(R) Portal ''' -__version__ = '1.4.0' +__version__ = '1.4.1' __author__ = 'Arista Networks, Inc.' diff --git a/cvprac/cvp_api.py b/cvprac/cvp_api.py index 0d73658..975b5c1 100644 --- a/cvprac/cvp_api.py +++ b/cvprac/cvp_api.py @@ -3930,6 +3930,15 @@ class CvpApi(): 'time': '2022-05-03T15:38:53.725014447Z', 'type': 'INITIAL'}, ...] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.cvp_version_compare('>=', 14.0, msg): + url = '/api/resources/serviceaccount/v1/Token/all' + self.log.debug(f"v14 {url}") + # Pull list of tokens out of data key of return for new resource APIs + resp = self.clnt.get(url) + tokens = [] + if "data" in resp: + tokens = resp["data"] + return tokens if self.cvp_version_compare('>=', 7.0, msg): url = '/api/v3/services/arista.serviceaccount.v1.TokenService/GetAll' self.log.debug(f"v7 {url}") @@ -3939,6 +3948,8 @@ class CvpApi(): 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. + Args: + token_id (string): The id of the service account token. Returns: response (list): Returns a list of dict that contains... Ex: [{'value': {'key': {'id': 'randomId'}, 'user': 'string', @@ -3947,11 +3958,16 @@ class CvpApi(): 'time': '2022-05-03T15:38:53.725014447Z', 'type': 'INITIAL'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.cvp_version_compare('>=', 14.0, msg): + endpoint = '/api/resources/serviceaccount/v1/Token' + query_param = f"?key.id={token_id}" + self.log.debug(f'v14 {endpoint + query_param}') + return self.clnt.get(endpoint + query_param) if self.cvp_version_compare('>=', 7.0, msg): + endpoint = '/api/v3/services/arista.serviceaccount.v1.TokenService/GetOne' payload = {"key": {"id": token_id}} - url = '/api/v3/services/arista.serviceaccount.v1.TokenService/GetOne' - self.log.debug(f"v7 {url} {payload}") - return self.clnt.post(url, data=payload) + self.log.debug(f'v7 {endpoint} {payload}') + return self.clnt.post(endpoint, data=payload) return None def svc_account_token_delete(self, token_id): @@ -3965,11 +3981,15 @@ class CvpApi(): 'time': '2022-07-26T15:29:03.687167871Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.cvp_version_compare('>=', 14.0, msg): + endpoint = f'/api/resources/serviceaccount/v1/TokenConfig?key.id={token_id}' + self.log.debug(f'v14 {endpoint}') + return self.clnt.delete(endpoint) if self.cvp_version_compare('>=', 7.0, msg): + endpoint = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Delete' payload = {"key": {"id": token_id}} - url = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Delete' - self.log.debug(f"v7 {url} {payload}") - return self.clnt.post(url, data=payload) + self.log.debug(f'v7 {endpoint} {payload}') + return self.clnt.post(endpoint, data=payload) return None def svc_account_token_set(self, username, duration, description): @@ -3987,14 +4007,31 @@ class CvpApi(): '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.cvp_version_compare('>=', 14.0, msg): + payload = { + 'key': { + 'id': '' + }, + 'description': description, + 'user': username, + 'validFor': duration, + 'token': '' + } + endpoint = '/api/resources/serviceaccount/v1/TokenConfig' + self.log.debug(f'v14 {endpoint} {payload}') + return self.clnt.post(endpoint, data=payload) if self.cvp_version_compare('>=', 7.0, msg): - url = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Set' - self.log.debug(f"v7 {url} {payload}") - return self.clnt.post(url, data=payload) + payload = { + 'value': { + 'description': description, + 'user': username, + 'valid_for': duration + } + } + endpoint = '/api/v3/services/arista.serviceaccount.v1.TokenConfigService/Set' + self.log.debug(f'v7 {endpoint} {payload}') + return self.clnt.post(endpoint, data=payload) return None def svc_account_get_all(self): @@ -4005,13 +4042,21 @@ class CvpApi(): 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.cvp_version_compare('>=', 14.0, msg): + endpoint = '/api/resources/serviceaccount/v1/Account/all' + self.log.debug(f"v14 {endpoint}") + # Pull list of accounts out of data key of return for new resource APIs + resp = self.clnt.get(endpoint) + svc_accounts = [] + if "data" in resp: + svc_accounts = resp["data"] + return svc_accounts if self.cvp_version_compare('>=', 7.0, msg): - url = '/api/v3/services/arista.serviceaccount.v1.AccountService/GetAll' - self.log.debug(f"v7 {url}") - return self.clnt.post(url) + endpoint = '/api/v3/services/arista.serviceaccount.v1.AccountService/GetAll' + self.log.debug(f"v7 {endpoint}") + return self.clnt.post(endpoint) return None def svc_account_get_one(self, username): @@ -4026,13 +4071,35 @@ class CvpApi(): 'time': '2022-02-10T04:28:14.251684869Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.cvp_version_compare('>=', 14.0, msg): + endpoint = '/api/resources/serviceaccount/v1/Account' + query_param = f"?key.name={username}" + self.log.debug(f"v14 {endpoint + query_param}") + return self.clnt.get(endpoint + query_param) if self.cvp_version_compare('>=', 7.0, msg): + endpoint = '/api/v3/services/arista.serviceaccount.v1.AccountService/GetOne' payload = {"key": {"name": username}} - url = '/api/v3/services/arista.serviceaccount.v1.AccountService/GetOne' - self.log.debug(f"v7 {url} {payload}") - return self.clnt.post(url, data=payload) + self.log.debug(f"v7 {endpoint} {payload}") + return self.clnt.post(endpoint, data=payload) return None + def _get_valid_role_ids(self, roles): + ''' Helper function to validate and retrieve role IDs based on provided roles. + Args: + roles (list): The list of role names. + Returns: + role_ids (list): The list of role IDs. + ''' + 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(f"Not all provided roles {roles} are valid. " + f"Only using the found valid roles {role_ids}") + return role_ids + 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. @@ -4056,23 +4123,26 @@ class CvpApi(): 'time': '2022-07-26T18:19:55.392173445Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + # Retrieve valid role IDs + role_ids = self._get_valid_role_ids(roles) + if self.cvp_version_compare('>=', 14.0, msg): + payload = { + 'description': description, + 'groups': {'values': role_ids}, + 'key': {'name': username}, + 'status': status + } + endpoint = '/api/resources/serviceaccount/v1/AccountConfig' + self.log.debug(f"v14 {endpoint} {payload}") + return self.clnt.post(endpoint, data=payload) if self.cvp_version_compare('>=', 7.0, 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(f"Not all provided roles {roles} are valid. " - f"Only using the found valid 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(f"v7 {url} {payload}") - return self.clnt.post(url, data=payload) + endpoint = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/Set' + self.log.debug(f"v7 {endpoint} {payload}") + return self.clnt.post(endpoint, data=payload) return None def svc_account_delete(self, username): @@ -4086,11 +4156,15 @@ class CvpApi(): 'time': '2022-07-26T18:26:53.637425846Z'}] ''' msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + if self.cvp_version_compare('>=', 14.0, msg): + endpoint = f'/api/resources/serviceaccount/v1/AccountConfig?key.name={username}' + self.log.debug(f"v14 {endpoint}") + return self.clnt.delete(endpoint) if self.cvp_version_compare('>=', 7.0, msg): payload = {"key": {"name": username}} - url = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/Delete' - self.log.debug(f"v7 {url} {payload}") - return self.clnt.post(url, data=payload) + endpoint = '/api/v3/services/arista.serviceaccount.v1.AccountConfigService/Delete' + self.log.debug(f"v7 {endpoint} {payload}") + return self.clnt.post(endpoint, data=payload) return None def svc_account_delete_expired_tokens(self): @@ -4105,11 +4179,21 @@ class CvpApi(): 'time': '2022-07-26T18:30:28.022504853Z','type': 'INITIAL'}, {'value': {'key': {'id': '2f6325d9c'},...] ''' + msg = 'Service Account Resource APIs are supported from 2021.3.0+.' + valid_until_format = "valid_until" + resource_api_schema = False + if self.cvp_version_compare('>=', 14.0, msg): + resource_api_schema = True + valid_until_format = "validUntil" 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']) + if resource_api_schema: + token_data = tok['result']['value'] + else: + token_data = tok['value'] + if (datetime.strptime(token_data[valid_until_format], "%Y-%m-%dT%H:%M:%SZ") < + datetime.utcnow()): + self.svc_account_token_delete(token_data['key']['id']) expired_tokens.append(tok) return expired_tokens diff --git a/cvprac/cvp_client.py b/cvprac/cvp_client.py index a02676f..3eb3cd6 100644 --- a/cvprac/cvp_client.py +++ b/cvprac/cvp_client.py @@ -123,7 +123,7 @@ class CvpClient(): # Maximum number of times to retry a get or post to the same # CVP node. NUM_RETRY_REQUESTS = 3 - LATEST_API_VERSION = 9.0 + LATEST_API_VERSION = 14.0 def __init__(self, logger='cvprac', syslog=False, filename=None, log_level='INFO'): @@ -213,7 +213,13 @@ class CvpClient(): For CVP versions 2020.2.4 through 2021.1.x, use api version 5.0 For CVP versions 2021.2.x, use api version 6.0 For CVP versions 2021.3.x, use api version 7.0 - For CVP versions 2022.1.0 and beyond, use api version 8.0 + For CVP versions 2022.x.x, use api version 8.0 + For CVP versions 2023.1.x, use api version 9.0 + For CVP versions 2023.2.x, use api version 10.0 + For CVP versions 2023.3.x, use api version 11.0 + For CVP versions 2024.1.x, use api version 12.0 + For CVP versions 2024.2.x, use api version 13.0 + For CVP versions 2024.3.x and beyond, use api version 14.0 Args: version (str): The CVP version in use. @@ -221,6 +227,11 @@ class CvpClient(): self.version = version self.log.info('Version %s', version) # Set apiversion to latest available API version for CVaaS + # Set apiversion to 14.0 for 2024.3.x + # Set apiversion to 13.0 for 2024.2.x + # Set apiversion to 12.0 for 2024.1.x + # Set apiversion to 11.0 for 2023.3.x + # Set apiversion to 10.0 for 2023.2.x # Set apiversion to 9.0 for 2023.1.x # Set apiversion to 8.0 for 2022.1.x - 2022.3.x # Set apiversion to 7.0 for 2021.3.x @@ -242,7 +253,13 @@ class CvpClient(): ' Appending 0. Updated Version String - %s', ".".join(version_components)) full_version = ".".join(version_components) - if parse(full_version) >= parse('2024.1.0'): + if parse(full_version) >= parse('2024.3.0'): + self.log.info('Setting API version to v14') + self.apiversion = 14.0 + elif parse(full_version) >= parse('2024.2.0'): + self.log.info('Setting API version to v13') + self.apiversion = 13.0 + elif parse(full_version) >= parse('2024.1.0'): self.log.info('Setting API version to v12') self.apiversion = 12.0 elif parse(full_version) >= parse('2023.3.0'): diff --git a/docs/release-notes-1.4.1.rst b/docs/release-notes-1.4.1.rst new file mode 100644 index 0000000..fb77a35 --- /dev/null +++ b/docs/release-notes-1.4.1.rst @@ -0,0 +1,11 @@ +###### +v1.4.1 +###### + +2025-5-8 + +Enhancements +^^^^^^^^^^^^ + +* Add support new service account resource APIs. (`281 `_) [`noredistribution `_] +* Updates for CVP 2024.3.0 suppot. (`282 `_) [`mharista `_]