From 8a2d855f620b9f88f16774b33b090ea92aa9a552 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 9 Aug 2020 22:06:09 +0100 Subject: [PATCH 001/204] my proposal for v2 pagination --- dynatrace/requests/request_handler.py | 98 +++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 3baf04d..1c63e09 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -122,6 +122,104 @@ def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method return response +def __get_multipage_results(cluster, endpoint, cursor, item, tenant=None): + """ + Private function: not intended for calling from outside of this module. + Retrieves subsequent pages of multi-page API call and gathers just the + items requested through the endpoint (e.g. entities, metrics, etc.) + \n + @param cluster - Cluster dictionary from variable_set\n + @param endpoint - API endpoint to call.\n + @param tenant - String of tenant name used in cluster dictionary\n + @param cursor - cursor that was returned with the first page of results\n + @param item - item being retrieved (e.g. entities, metrics, etc.) + """ + results_full = [] + while cursor: + results_page = make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=endpoint, + params=dict(nextPageKey=cursor) + ).json() + + # Collect just the items being queried + results_full.extend(results_page.get(item)) + + # Renew cursor + cursor = results_page.get('nextPageKey') + + return results_full + + +def v2_get_results_whole(cluster, endpoint, item, tenant=None, params=None): + """ + Gets a multi-paged result set and returns it whole. To be used with V2 API + pagination where the nextPageKey is returned in the body of the response. + \n + @param item - item being retrieved (e.g. entities, metrics, etc.)\n + @param cluster - Cluster dictionary from variable_set\n + @param endpoint - API endpoint to call.\n + @param tenant - String of tenant name used in cluster dictionary\n + @param cursor - cursor that was returned with the first page of results\n + @param params - dictionary of query string parameters + """ + # Get the first results set (including cursor) + response = make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=endpoint, + params=params + ).json() + + # In the case of multi-page, get the rest + cursor = response.get('nextPageKey') + if cursor: + response[item].extend(__get_multipage_results( + cluster=cluster, + endpoint=endpoint, + tenant=tenant, + cursor=cursor, + item=item + )) + + return response + + +def v2_get_results_by_page(cluster, endpoint, item, tenant=None, params=None): + """ + Gets a multi-paged result set one page at a time. To be used with V2 API + pagination where the nextPageKey is returned in the body of the response. + \n + @param item - item being retrieved (e.g. entities, metrics, etc.)\n + @param cluster - Cluster dictionary from variable_set\n + @param endpoint - API endpoint to call.\n + @param tenant - String of tenant name used in cluster dictionary\n + @param params - dictionary of query string parameters + """ + # Make the first API call; retrieve summary info and first page of results + response = make_api_call( + cluster=cluster, + endpoint=endpoint, + tenant=tenant, + params=params + ).json() + cursor = response.get('nextPageKey') + # Pause here and return 1st page + yield response + + # On subsequent calls, yield page by page the remaining result set + while cursor: + response = make_api_call( + cluster=cluster, + endpoint=endpoint, + tenant=tenant, + params=dict(nextPageKey=cursor) + ).json() + yield response + cursor = response.get('nextPageKey') + + def check_response(response): ''' Checks if the Response has a Successful Status Code From e7b52f27560842eab1d2e739e9452e9aedc68719 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 9 Aug 2020 22:22:04 +0100 Subject: [PATCH 002/204] refactored multipage results function to v2 --- dynatrace/requests/request_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 1c63e09..d453de1 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -122,7 +122,7 @@ def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method return response -def __get_multipage_results(cluster, endpoint, cursor, item, tenant=None): +def __get_v2_multipage_results(cluster, endpoint, cursor, item, tenant=None): """ Private function: not intended for calling from outside of this module. Retrieves subsequent pages of multi-page API call and gathers just the From a46fae340577a09e1b28b62236c4850373ef5f81 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 9 Aug 2020 22:47:57 +0100 Subject: [PATCH 003/204] added my proposal for v1 api pagination --- dynatrace/requests/request_handler.py | 56 ++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index d453de1..9b2d077 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -156,12 +156,13 @@ def v2_get_results_whole(cluster, endpoint, item, tenant=None, params=None): """ Gets a multi-paged result set and returns it whole. To be used with V2 API pagination where the nextPageKey is returned in the body of the response. + Also this type of query requires the queried item so we can extract it from + the subsequent pages and omit the summary data. \n @param item - item being retrieved (e.g. entities, metrics, etc.)\n @param cluster - Cluster dictionary from variable_set\n @param endpoint - API endpoint to call.\n @param tenant - String of tenant name used in cluster dictionary\n - @param cursor - cursor that was returned with the first page of results\n @param params - dictionary of query string parameters """ # Get the first results set (including cursor) @@ -186,6 +187,59 @@ def v2_get_results_whole(cluster, endpoint, item, tenant=None, params=None): return response +def v1_get_results_whole(cluster, endpoint, tenant=None, params=None): + """ + Gets a multi-paged result set and returns it whole. To be used with V1 API + pagination where the next-page-key is returned in the response headers. + \n + @param cluster - Cluster dictionary from variable_set\n + @param endpoint - API endpoint to call.\n + @param tenant - String of tenant name used in cluster dictionary\n + @param params - dictionary of query string parameters + """ + results = [] + # We'll always make at least 1 call + cursor = 1 + while cursor: + if cursor != 1: + params['nextPageKey'] = cursor + response = make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=endpoint, + params=params + ) + results.extend(response.json()) + cursor = response.headers.get('next-page-key') + + return results + + +def v1_get_results_by_page(cluster, endpoint, tenant=None, params=None): + """ + Gets a multi-paged result set one page at a time. To be used with V1 API + pagination where the next-page-key is returned in the response headers. + \n + @param cluster - Cluster dictionary from variable_set\n + @param endpoint - API endpoint to call.\n + @param tenant - String of tenant name used in cluster dictionary\n + @param params - dictionary of query string parameters + """ + cursor = 1 + while cursor: + if cursor != 1: + params['nextPageKey'] = cursor + response = make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=endpoint, + params=params + ) + # Pause here and return this page of results + yield response.json() + cursor = response.headers.get('next-page-key') + + def v2_get_results_by_page(cluster, endpoint, item, tenant=None, params=None): """ Gets a multi-paged result set one page at a time. To be used with V2 API From 3701947daf2c86ff11299bf6ad73fd44936efb22 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 9 Aug 2020 23:32:32 +0100 Subject: [PATCH 004/204] added decorator for slowing down requests --- dynatrace/requests/request_handler.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 9b2d077..62fe66e 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -1,6 +1,7 @@ """Make API Request to available Dynatrace API""" import requests import time +import functools from dynatrace.exceptions import InvalidAPIResponseException, ManagedClusterOnlyException from enum import Enum, auto @@ -80,6 +81,30 @@ def __repr__(self): return self.name +def slow_down(func): + """ + Decorator for slowing down API requests. In case of SaaS limits are as low + as 50/min. If current call is within the last 25% remaining requests (until + the limit is reached) then a slow down of 1 sec is applied. + *** Should only use to decorate API-calling functions *** + """ + @functools.wraps(func) + def wrapper(*args, **kwargs): + response = func(*args, **kwargs) + + # Standard Dynatrace response headers + req_remaining = int(response.headers.get('x-ratelimit-remaining')) + req_limit = int(response.headers.get('x-ratelimit-limit')) + # If 75% requests already made, slow down + print(f"{req_remaining} = {(req_remaining/req_limit)*100}%") + if req_remaining/req_limit <= 0.25: + time.sleep(1) + + return response + return wrapper + + +@slow_down def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method=HTTP.GET): ''' Function makes an API call in a safe way, taking into account the rate limits. From 533a567cb163eed0850637bab4ef12fcd2799cd8 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 9 Aug 2020 23:36:03 +0100 Subject: [PATCH 005/204] refactored all occurences of __get_multipage_results --- dynatrace/requests/request_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 9b2d077..16e5d6a 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -176,7 +176,7 @@ def v2_get_results_whole(cluster, endpoint, item, tenant=None, params=None): # In the case of multi-page, get the rest cursor = response.get('nextPageKey') if cursor: - response[item].extend(__get_multipage_results( + response[item].extend(__get_v2_multipage_results( cluster=cluster, endpoint=endpoint, tenant=tenant, From 6e639c784117ee613da6d34a0f78070eb03cacdf Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 9 Aug 2020 23:37:02 +0100 Subject: [PATCH 006/204] refactored all occurences of __get_multipage_results --- dynatrace/requests/request_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 62fe66e..7bb24b7 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -201,7 +201,7 @@ def v2_get_results_whole(cluster, endpoint, item, tenant=None, params=None): # In the case of multi-page, get the rest cursor = response.get('nextPageKey') if cursor: - response[item].extend(__get_multipage_results( + response[item].extend(__get_v2_multipage_results( cluster=cluster, endpoint=endpoint, tenant=tenant, From e736e3a89d490a67abb90a4151130b029b377e66 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Mon, 10 Aug 2020 08:30:34 +0100 Subject: [PATCH 007/204] squished v2_get_results_by_page --- dynatrace/requests/request_handler.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 7bb24b7..f6b42bb 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -265,36 +265,30 @@ def v1_get_results_by_page(cluster, endpoint, tenant=None, params=None): cursor = response.headers.get('next-page-key') -def v2_get_results_by_page(cluster, endpoint, item, tenant=None, params=None): +def v2_get_results_by_page(cluster, endpoint, tenant=None, params=None): """ Gets a multi-paged result set one page at a time. To be used with V2 API pagination where the nextPageKey is returned in the body of the response. \n - @param item - item being retrieved (e.g. entities, metrics, etc.)\n @param cluster - Cluster dictionary from variable_set\n @param endpoint - API endpoint to call.\n @param tenant - String of tenant name used in cluster dictionary\n @param params - dictionary of query string parameters """ - # Make the first API call; retrieve summary info and first page of results - response = make_api_call( - cluster=cluster, - endpoint=endpoint, - tenant=tenant, - params=params - ).json() - cursor = response.get('nextPageKey') - # Pause here and return 1st page - yield response - - # On subsequent calls, yield page by page the remaining result set + # We'll always make at least 1 call + cursor = 1 while cursor: + # On subsequent calls, must omit all other params + if cursor != 1: + params = dict(nextPageKey=cursor) + response = make_api_call( cluster=cluster, endpoint=endpoint, tenant=tenant, - params=dict(nextPageKey=cursor) + params=params ).json() + yield response cursor = response.get('nextPageKey') From 1e08bc5423e6ff752b8116bcbbceaeefd5f1be20 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 11:31:35 -0500 Subject: [PATCH 008/204] PAF-44 #Adding Settings and test cases. --- .circleci/config.yml | 5 ++- dynatrace/settings.py | 41 +++++++++++++++++++++ tests/special_test_settings_no_variables.py | 11 ++++++ tests/test_settings.py | 14 +++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 dynatrace/settings.py create mode 100644 tests/special_test_settings_no_variables.py create mode 100644 tests/test_settings.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 20c1262..8add716 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,7 +20,10 @@ jobs: pipenv install --dev - run: command: | - pipenv run python -m unittest discover + COVERAGE_FILE=standard.coverage pipenv run coverage -m unittest discover + COVERAGE_FILE=special1.coverage pipenv run coverage -m unittest special_test_settings_no_variables.py + pipenv run coverage combine *.coverage + pipenv run coverage html -d test-results - store_test_results: path: test-results - store_artifacts: diff --git a/dynatrace/settings.py b/dynatrace/settings.py new file mode 100644 index 0000000..5d55e72 --- /dev/null +++ b/dynatrace/settings.py @@ -0,0 +1,41 @@ +try: + import user_variables + FILE_IMPORTED=True +except ImportError: + FILE_IMPORTED=False + +class DefaultSettings(): + LOG_LEVEL = None + LOG_DIR = "logs/" + + # ROLE TYPE KEYS + # access_env + # change_settings + # install_agent + # view_logs + # view_senstive + # change_sensitive + + USER_GROUPS = { + "role_types":{ + "access_env": "accessenv", + "change_settings": "changesettings", + "view_logs": "logviewer", + "view_sensitive": "viewsensitive" + }, + "role_tenants":[ + "nonprod", + "prod" + ] + } + USER_GROUP_TEMPLATE = "prefix_{USER_TYPE}_{TENANT}_{APP_NAME}_suffix" + DEFAULT_TIMEZONE = "UTC" + + +def get_setting(attribute): + if FILE_IMPORTED and hasattr(user_variables, attribute): + return getattr(user_variables,attribute) + elif hasattr(DefaultSettings, attribute) : + return getattr(DefaultSettings, attribute) + else: + raise AttributeError(f"{attribute} is not a valid user variable attribute!") diff --git a/tests/special_test_settings_no_variables.py b/tests/special_test_settings_no_variables.py new file mode 100644 index 0000000..d2eeb2a --- /dev/null +++ b/tests/special_test_settings_no_variables.py @@ -0,0 +1,11 @@ +import unittest +from os import rename + +class TestSettingsWithoutVarFile(unittest.TestCase): + def test_settings_without_var_file(self): + rename("user_variables.py", "user_variables.py.tmp") + from dynatrace import settings + tz = settings.get_setting("DEFAULT_TIMEZONE") + self.assertEqual(tz, "UTC") + rename("user_variables.py.tmp", "user_variables.py") + diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..3cbf84a --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,14 @@ +import unittest +from dynatrace import settings + +class TestSettings(unittest.TestCase): + def test_get_setting_from_user_variable(self): + tz = settings.get_setting("DEFAULT_TIMEZONE") + self.assertEqual(tz, "America/Chicago") + def test_get_setting_from_default(self): + log_dir = settings.get_setting("LOG_DIR") + self.assertEqual(log_dir, "logs/") + def test_get_invalid_setting(self): + with self.assertRaises(AttributeError) as context: + settings.get_setting("INVALID_SETTING") + self.assertIn("not a valid user variable attribute", str(context.exception)) From 61401878e6993e6f908b5074502e8457ddce5904 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 11:34:30 -0500 Subject: [PATCH 009/204] PAF-44 #Tweaking and fixing updates to circleci --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8add716..5e294cd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,8 +20,8 @@ jobs: pipenv install --dev - run: command: | - COVERAGE_FILE=standard.coverage pipenv run coverage -m unittest discover - COVERAGE_FILE=special1.coverage pipenv run coverage -m unittest special_test_settings_no_variables.py + COVERAGE_FILE=standard.coverage pipenv run coverage run -m unittest discover + COVERAGE_FILE=special1.coverage pipenv run coverage run -m unittest special_test_settings_no_variables.py pipenv run coverage combine *.coverage pipenv run coverage html -d test-results - store_test_results: From 50fdd36bf9df3814c40a3e3628b27664ddde9283 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 11:36:21 -0500 Subject: [PATCH 010/204] PAF-44 #More fixes to CircleCI --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5e294cd..c1f3c5e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -21,7 +21,7 @@ jobs: - run: command: | COVERAGE_FILE=standard.coverage pipenv run coverage run -m unittest discover - COVERAGE_FILE=special1.coverage pipenv run coverage run -m unittest special_test_settings_no_variables.py + COVERAGE_FILE=special1.coverage pipenv run coverage run -m unittest tests/special_test_settings_no_variables.py pipenv run coverage combine *.coverage pipenv run coverage html -d test-results - store_test_results: From 0ce9d9eb5f3ff8502e8b1771dfa3491288543ee2 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 11:48:56 -0500 Subject: [PATCH 011/204] PAF-44 #Updating coveragerc for CircleCI --- .coveragerc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index b961959..93a6d95 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,7 +1,7 @@ [run] omit = # omit anything in a .local directory anywhere - */.local/* + */lib/python*/* # omit everything in /usr /usr/* # omit in the test tools From a77320e02df4c18134bfb37354a1e2eb39a2b278 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 11:52:27 -0500 Subject: [PATCH 012/204] PAF-44 #taring the result for easy retreival --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c1f3c5e..a9f51c2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,8 @@ jobs: COVERAGE_FILE=standard.coverage pipenv run coverage run -m unittest discover COVERAGE_FILE=special1.coverage pipenv run coverage run -m unittest tests/special_test_settings_no_variables.py pipenv run coverage combine *.coverage - pipenv run coverage html -d test-results + pipenv run coverage html + tar -cf test-results/coverage_report.tar - store_test_results: path: test-results - store_artifacts: From 5da63d522665fc36de46343fc871bd7a48f1bb78 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 11:55:36 -0500 Subject: [PATCH 013/204] PAF-44 #Tar fixes --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a9f51c2..b94ab3f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ jobs: COVERAGE_FILE=special1.coverage pipenv run coverage run -m unittest tests/special_test_settings_no_variables.py pipenv run coverage combine *.coverage pipenv run coverage html - tar -cf test-results/coverage_report.tar + tar -cf test-results/coverage_report.tar htmlcov - store_test_results: path: test-results - store_artifacts: From 04c95c7e259e890a335aa7e9bede897500cae466 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 11:58:08 -0500 Subject: [PATCH 014/204] #PAF-44 Creating Dir for tar --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b94ab3f..4b229aa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,8 @@ jobs: COVERAGE_FILE=special1.coverage pipenv run coverage run -m unittest tests/special_test_settings_no_variables.py pipenv run coverage combine *.coverage pipenv run coverage html - tar -cf test-results/coverage_report.tar htmlcov + mkdir test-results + tar -cf test-results/coverage_report.tar htmlcov/ - store_test_results: path: test-results - store_artifacts: From 28697fdf225564b1cc3fe543ab47922befc0e7c0 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 13:36:52 -0500 Subject: [PATCH 015/204] #PAF-46 Fixing from Lint --- .circleci/mockserver.py | 2 +- dynatrace/settings.py | 16 +++++++++------- dynatrace/tenant/maintenance.py | 2 +- dynatrace/tenant/request_attributes.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.circleci/mockserver.py b/.circleci/mockserver.py index acfb7fd..96fd562 100644 --- a/.circleci/mockserver.py +++ b/.circleci/mockserver.py @@ -14,7 +14,7 @@ } } -LOG_LEVEL="INFO" +LOG_LEVEL = "INFO" # ROLE TYPE KEYS # access_env diff --git a/dynatrace/settings.py b/dynatrace/settings.py index 5d55e72..d96386c 100644 --- a/dynatrace/settings.py +++ b/dynatrace/settings.py @@ -1,8 +1,9 @@ try: import user_variables - FILE_IMPORTED=True + FILE_IMPORTED = True except ImportError: - FILE_IMPORTED=False + FILE_IMPORTED = False + class DefaultSettings(): LOG_LEVEL = None @@ -17,13 +18,13 @@ class DefaultSettings(): # change_sensitive USER_GROUPS = { - "role_types":{ + "role_types": { "access_env": "accessenv", "change_settings": "changesettings", "view_logs": "logviewer", "view_sensitive": "viewsensitive" }, - "role_tenants":[ + "role_tenants": [ "nonprod", "prod" ] @@ -34,8 +35,9 @@ class DefaultSettings(): def get_setting(attribute): if FILE_IMPORTED and hasattr(user_variables, attribute): - return getattr(user_variables,attribute) - elif hasattr(DefaultSettings, attribute) : + return getattr(user_variables, attribute) + elif hasattr(DefaultSettings, attribute): return getattr(DefaultSettings, attribute) else: - raise AttributeError(f"{attribute} is not a valid user variable attribute!") + raise AttributeError( + f"{attribute} is not a valid user variable attribute!") diff --git a/dynatrace/tenant/maintenance.py b/dynatrace/tenant/maintenance.py index c1aef4e..e40ddb3 100644 --- a/dynatrace/tenant/maintenance.py +++ b/dynatrace/tenant/maintenance.py @@ -15,7 +15,7 @@ class Suppression(Enum): Types of suppression for create Maintenance Window JSON. Suppression is required Args: - Enum (DETECT_PROBLEMS_AND_ALERT): Full Alerting. Entites in scope will have notes that a Maintenance Window was active + Enum (DETECT_PROBLEMS_AND_ALERT): Full Detection and Alerting during Maintenance Window Enum (DETECT_PROBLEMS_DONT_ALERT): Problems detected but alerting profiles in that scope are not triggered Enum (DONT_DETECT_PROBLEMS): Problem detection completely off for the scope """ diff --git a/dynatrace/tenant/request_attributes.py b/dynatrace/tenant/request_attributes.py index d1340ac..bf77e56 100644 --- a/dynatrace/tenant/request_attributes.py +++ b/dynatrace/tenant/request_attributes.py @@ -9,7 +9,7 @@ def pull_to_files(cluster, tenant, ignore_disabled=True): """Pull files from an environment to local""" # API Calls needed: Pull RA, take the ID and pull the details of each RA - all_ra_call = rh.make_api_call(cluster=cluster, + all_ra_call = rh.make_api_call(cluster=cluster, tenant=tenant, endpoint=ENDPOINT) all_ra_json = all_ra_call.json() From 23ae59869229c40042a0243f6f1bef4a1f512e5f Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 14:00:29 -0500 Subject: [PATCH 016/204] PAF-46 #FLAKE8 Linting --- dynatrace/exceptions.py | 1 + dynatrace/tenant/topology/__init__.py | 7 - tests/test_maintenance_windows.py | 190 ++++++++++++++------------ 3 files changed, 107 insertions(+), 91 deletions(-) diff --git a/dynatrace/exceptions.py b/dynatrace/exceptions.py index 48367a8..25ed096 100644 --- a/dynatrace/exceptions.py +++ b/dynatrace/exceptions.py @@ -13,6 +13,7 @@ class InvalidDateFormatException(ValueError): def __init__(self, required_format): self.message = f"Incorrect Date for following entry: {required_format}" + class InvalidScopeException(ValueError): def __init__(self, required_format): self.required_format = required_format diff --git a/dynatrace/tenant/topology/__init__.py b/dynatrace/tenant/topology/__init__.py index 17f1f19..e69de29 100644 --- a/dynatrace/tenant/topology/__init__.py +++ b/dynatrace/tenant/topology/__init__.py @@ -1,7 +0,0 @@ -from dynatrace.tenant.topology.applications import * -from dynatrace.tenant.topology.custom import * -from dynatrace.tenant.topology.hosts import * -from dynatrace.tenant.topology.process import * -from dynatrace.tenant.topology.process_groups import * -from dynatrace.tenant.topology.services import * -from dynatrace.tenant.topology.shared import * diff --git a/tests/test_maintenance_windows.py b/tests/test_maintenance_windows.py index e76709a..34262f2 100644 --- a/tests/test_maintenance_windows.py +++ b/tests/test_maintenance_windows.py @@ -178,7 +178,7 @@ def test_create_once_no_scope(self): ) maintenance_schedule = maintenance.generate_schedule( maintenance.RecurrenceType.ONCE, - #TODO Remove need for these variables. ONCE does not use them + # TODO Remove need for these variables. ONCE does not use them "23:00", 60, TEST_RANGE_START, @@ -210,7 +210,7 @@ def test_create_weekly_no_scope(self): ) maintenance_schedule = maintenance.generate_schedule( maintenance.RecurrenceType.WEEKLY, - #TODO Remove need for these variables. ONCE does not use them + # TODO Remove need for these variables. ONCE does not use them "23:00", 60, TEST_RANGE_START, @@ -243,7 +243,7 @@ def test_create_monthly_no_scope(self): ) maintenance_schedule = maintenance.generate_schedule( maintenance.RecurrenceType.MONTHLY, - #TODO Remove need for these variables. ONCE does not use them + # TODO Remove need for these variables. ONCE does not use them "23:00", 60, TEST_RANGE_START, @@ -261,41 +261,44 @@ def test_create_monthly_no_scope(self): self.assertEqual(result, tooling_for_test.expected_payload( mockserver_response_file)) + class TestMaintenanceExceptions(unittest.TestCase): def test_invalid_recurrence_type(self): """Testing exception thrown for invalid recurrence type""" with self.assertRaises(ValueError) as context: maintenance.generate_schedule( - "HOURLY", - "23:00", - 60, - TEST_RANGE_START, - TEST_RANGE_END, + "HOURLY", + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, ) + self.assertTrue("Invalid Recurrence Type!" in str(context.exception)) + def test_invalid_day_of_week(self): """Testing exception thrown for invalid dayOfWeek""" with self.assertRaises(ValueError) as context: maintenance.generate_schedule( - maintenance.RecurrenceType.WEEKLY, - "23:00", - 60, - TEST_RANGE_START, - TEST_RANGE_END, - day=1 + maintenance.RecurrenceType.WEEKLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + day=1 ) self.assertTrue("Invalid Weekly Day!" in str(context.exception)) - + def test_invalid_day_of_month_value(self): """Testing exception thrown for invalid dayOfMonth for incorrect int""" with self.assertRaises(ValueError) as context: maintenance.generate_schedule( - maintenance.RecurrenceType.MONTHLY, - "23:00", - 60, - TEST_RANGE_START, - TEST_RANGE_END, - day=32 + maintenance.RecurrenceType.MONTHLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + day=32 ) self.assertTrue("Invalid Monthly Day!" in str(context.exception)) @@ -303,24 +306,25 @@ def test_invalid_day_of_month_type(self): """Testing exception thrown for invalid dayOfMonth for a non-int""" with self.assertRaises(TypeError) as context: maintenance.generate_schedule( - maintenance.RecurrenceType.MONTHLY, - "23:00", - 60, - TEST_RANGE_START, - TEST_RANGE_END, - day="Eleven" + maintenance.RecurrenceType.MONTHLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + day="Eleven" ) - self.assertTrue("Invalid type for Day of Month! Int between 1-31 required" in str(context.exception)) + self.assertTrue( + "Invalid type for Day of Month! Int between 1-31 required" in str(context.exception)) def test_no_day_of_week_supplied(self): """Weekly Maintenance Window with no dayOfWeek supplied""" with self.assertRaises(Exception) as context: maintenance.generate_schedule( - maintenance.RecurrenceType.WEEKLY, - "23:00", - 60, - TEST_RANGE_START, - TEST_RANGE_END, + maintenance.RecurrenceType.WEEKLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, ) self.assertTrue("Invalid Weekly Day!" in str(context.exception)) @@ -328,44 +332,52 @@ def test_no_day_of_month_supplied(self): """Monthly Maintenance Window with no dayOfMonth supplied""" with self.assertRaises(Exception) as context: maintenance.generate_schedule( - maintenance.RecurrenceType.MONTHLY, - "23:00", - 60, - TEST_RANGE_START, - TEST_RANGE_END, + maintenance.RecurrenceType.MONTHLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, ) - self.assertTrue("Invalid type for Day of Month!" in str(context.exception)) + self.assertTrue( + "Invalid type for Day of Month!" in str(context.exception)) def test_invalid_datetime_format(self): """Test invalid datetime supplied to trigger ValueError""" - #TODO Fix Exceoption to have a message as first arg + # TODO Fix Exceoption to have a message as first arg with self.assertRaises(InvalidDateFormatException) as context: maintenance.generate_schedule( - maintenance.RecurrenceType.DAILY, - "23:00", - 60, - TEST_RANGE_START, - "2020-01-02" + maintenance.RecurrenceType.DAILY, + "23:00", + 60, + TEST_RANGE_START, + "2020-01-02" ) - self.assertTrue("Incorrect Date " in context.exception.message, context.exception.message) + self.assertTrue( + "Incorrect Date " in context.exception.message, context.exception.message) + def test_invalid_filter_type(self): """Invalid Filter_Type""" with self.assertRaises(ValueError) as context: maintenance.generate_scope( - tags=[{'context': "CONTEXTLESS", 'key': "testing"}], - filter_type="INVALID_TYPE" + tags=[{'context': "CONTEXTLESS", 'key': "testing"}], + filter_type="INVALID_TYPE" ) - self.assertTrue("Invalid Filter Type" in (msg := str(context.exception)), msg) + self.assertTrue("Invalid Filter Type" in ( + msg := str(context.exception)), msg) class TestMaintenanceEnumTypes(unittest.TestCase): def test_suppression_enum_str(self): - suppression = maintenance.Suppression(maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) - self.assertIsInstance(maintenance.Suppression.__str__(suppression), str) + suppression = maintenance.Suppression( + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) + self.assertIsInstance( + maintenance.Suppression.__str__(suppression), str) def test_suppression_enum_repr(self): - suppression = maintenance.Suppression(maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) - self.assertIsInstance(maintenance.Suppression.__repr__(suppression), str) + suppression = maintenance.Suppression( + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) + self.assertIsInstance( + maintenance.Suppression.__repr__(suppression), str) def test_day_of_week_enum_str(self): day_of_week = maintenance.DayOfWeek(maintenance.DayOfWeek.MONDAY) @@ -384,20 +396,27 @@ def test_context_enum_repr(self): self.assertIsInstance(maintenance.Context.__repr__(context), str) def test_recurrence_type_enum_str(self): - recurrence_type = maintenance.RecurrenceType(maintenance.RecurrenceType.DAILY) - self.assertIsInstance(maintenance.RecurrenceType.__str__(recurrence_type), str) + recurrence_type = maintenance.RecurrenceType( + maintenance.RecurrenceType.DAILY) + self.assertIsInstance( + maintenance.RecurrenceType.__str__(recurrence_type), str) def test_recurrence_type_enum_repr(self): - recurrence_type = maintenance.RecurrenceType(maintenance.RecurrenceType.DAILY) - self.assertIsInstance(maintenance.RecurrenceType.__repr__(recurrence_type), str) + recurrence_type = maintenance.RecurrenceType( + maintenance.RecurrenceType.DAILY) + self.assertIsInstance( + maintenance.RecurrenceType.__repr__(recurrence_type), str) def test_filter_type_enum_str(self): - suppression = maintenance.FilterType(maintenance.FilterType.APM_SECURITY_GATEWAY) + suppression = maintenance.FilterType( + maintenance.FilterType.APM_SECURITY_GATEWAY) self.assertIsInstance(maintenance.FilterType.__str__(suppression), str) def test_filter_type_enum_repr(self): - suppression = maintenance.FilterType(maintenance.FilterType.APM_SECURITY_GATEWAY) - self.assertIsInstance(maintenance.FilterType.__repr__(suppression), str) + suppression = maintenance.FilterType( + maintenance.FilterType.APM_SECURITY_GATEWAY) + self.assertIsInstance( + maintenance.FilterType.__repr__(suppression), str) class TestTagParsing(unittest.TestCase): @@ -413,38 +432,41 @@ def test_tag_variations(self): # Test 8 - Context, Key with square brackets # Test 9 - Context, Key with colon and squares # Test 10 - Empty Context with squares - + test_tag_list = [ - "Key", - "Key:Value", - "[Context]Key:Value", - "Key:withColon:Value", - "Key:withColon:", - "[Context]Key:withColon:Value", - "[Context]Key", - "[Context][KeywithSquares]", - "[Context][KeyWithSquares]:AndColons:Value", - "[][KeywithSquares]", + "Key", + "Key:Value", + "[Context]Key:Value", + "Key:withColon:Value", + "Key:withColon:", + "[Context]Key:withColon:Value", + "[Context]Key", + "[Context][KeywithSquares]", + "[Context][KeyWithSquares]:AndColons:Value", + "[][KeywithSquares]", ] test_tag_expected_results = [ - {'context': 'CONTEXTLESS', 'key': 'Key'}, - {'context': 'CONTEXTLESS', 'key': 'Key:Value'}, - {'context': 'Context', 'key': 'Key:Value'}, - {'context': 'CONTEXTLESS', 'key': 'Key:withColon:Value'}, - {'context': 'CONTEXTLESS', 'key': 'Key:withColon:'}, - {'context': 'Context', 'key': 'Key:withColon:Value'}, - {'context': 'Context', 'key': 'Key'}, - {'context': 'Context', 'key': '[KeywithSquares]'}, - {'context': 'Context', 'key': '[KeyWithSquares]:AndColons:Value'}, - {'context': 'CONTEXTLESS', 'key': '[][KeywithSquares]'}, + {'context': 'CONTEXTLESS', 'key': 'Key'}, + {'context': 'CONTEXTLESS', 'key': 'Key:Value'}, + {'context': 'Context', 'key': 'Key:Value'}, + {'context': 'CONTEXTLESS', 'key': 'Key:withColon:Value'}, + {'context': 'CONTEXTLESS', 'key': 'Key:withColon:'}, + {'context': 'Context', 'key': 'Key:withColon:Value'}, + {'context': 'Context', 'key': 'Key'}, + {'context': 'Context', 'key': '[KeywithSquares]'}, + {'context': 'Context', + 'key': '[KeyWithSquares]:AndColons:Value'}, + {'context': 'CONTEXTLESS', 'key': '[][KeywithSquares]'}, ] for i in range(0, len(test_tag_list)): processed_tag = test_tag_list[i] self.assertTrue( - (result := maintenance.parse_tag(processed_tag)) == test_tag_expected_results[i], - f"Test {i}: {result} did not match {test_tag_expected_results[i]}") + (result := maintenance.parse_tag(processed_tag) + ) == test_tag_expected_results[i], + f"Test {i}: {result} did not match {test_tag_expected_results[i]}") + if __name__ == '__main__': unittest.main() @@ -465,4 +487,4 @@ def test_tag_variations(self): # GET ALL WINDOWS # GET DETAILS OF WINDOW # DELETE WINDOW -# UPDATE WINDOW \ No newline at end of file +# UPDATE WINDOW From 5b23c05faa444c528aba0df8db6b4dad1252332f Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 14:07:32 -0500 Subject: [PATCH 017/204] #PAF-46 Linting for FLAKE8 --- dynatrace/tenant/topology/shared.py | 2 +- tests/special_test_settings_no_variables.py | 4 +++- tests/test_settings.py | 7 ++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/dynatrace/tenant/topology/shared.py b/dynatrace/tenant/topology/shared.py index 32f35ef..0148877 100644 --- a/dynatrace/tenant/topology/shared.py +++ b/dynatrace/tenant/topology/shared.py @@ -135,4 +135,4 @@ def add_env_layer_tags(cluster, tenant, layer, entity, tag_list): tag_json = { 'tags': tag_list } - return set_env_layer_properties(cluster, tenant, layer, entity, tag_json) \ No newline at end of file + return set_env_layer_properties(cluster, tenant, layer, entity, tag_json) diff --git a/tests/special_test_settings_no_variables.py b/tests/special_test_settings_no_variables.py index d2eeb2a..d9fe603 100644 --- a/tests/special_test_settings_no_variables.py +++ b/tests/special_test_settings_no_variables.py @@ -1,11 +1,13 @@ import unittest from os import rename + class TestSettingsWithoutVarFile(unittest.TestCase): + """Special test cases when user_variables is absent""" def test_settings_without_var_file(self): + """Test should return default value when user_variables missing""" rename("user_variables.py", "user_variables.py.tmp") from dynatrace import settings tz = settings.get_setting("DEFAULT_TIMEZONE") self.assertEqual(tz, "UTC") rename("user_variables.py.tmp", "user_variables.py") - diff --git a/tests/test_settings.py b/tests/test_settings.py index 3cbf84a..5873f54 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,14 +1,19 @@ import unittest from dynatrace import settings + class TestSettings(unittest.TestCase): def test_get_setting_from_user_variable(self): tz = settings.get_setting("DEFAULT_TIMEZONE") self.assertEqual(tz, "America/Chicago") + def test_get_setting_from_default(self): log_dir = settings.get_setting("LOG_DIR") self.assertEqual(log_dir, "logs/") + def test_get_invalid_setting(self): with self.assertRaises(AttributeError) as context: settings.get_setting("INVALID_SETTING") - self.assertIn("not a valid user variable attribute", str(context.exception)) + + self.assertIn("not a valid user variable attribute", + str(context.exception)) From 3c1e78d7a720371a434361e8ea1067fb966a5b82 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 14:13:34 -0500 Subject: [PATCH 018/204] PAF-46 Resolving JSON Lint. Removing unneeded file --- tests/mockserver_payloads/responses/get_all.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/mockserver_payloads/responses/get_all.json diff --git a/tests/mockserver_payloads/responses/get_all.json b/tests/mockserver_payloads/responses/get_all.json deleted file mode 100644 index e69de29..0000000 From 30c7ef4c2aebbbcf7a468aa8fd6555a2c20737f1 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 14:24:15 -0500 Subject: [PATCH 019/204] Trying to use pylintrc --- .github/workflows/linter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index d093e8a..c1e3cc1 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -49,3 +49,4 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PYTHON_PYLINT_CONFIG_FILE: .pylintrc From 93f986d25ca95bbde98070b0120d9aa79a7320f3 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 14:29:28 -0500 Subject: [PATCH 020/204] #PAF-46 Trying to reference linter in root --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index c1e3cc1..284300f 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -49,4 +49,4 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PYTHON_PYLINT_CONFIG_FILE: .pylintrc + PYTHON_PYLINT_CONFIG_FILE: ~/.pylintrc From 4fe2941ac845a2efa8bbce57cb7ab68066da02c0 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 14:36:36 -0500 Subject: [PATCH 021/204] PAF-46 #Trying to reference workspace root --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 284300f..69573ae 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -49,4 +49,4 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PYTHON_PYLINT_CONFIG_FILE: ~/.pylintrc + PYTHON_PYLINT_CONFIG_FILE: $GITHUB_WORKSPACE/.pylintrc From 9d7d0ab90939bdaad8a38033e8e9cc7ccc3cb611 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 14:45:46 -0500 Subject: [PATCH 022/204] PAF-46 --- .github/workflows/linter.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 69573ae..009e828 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -49,4 +49,5 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PYTHON_PYLINT_CONFIG_FILE: $GITHUB_WORKSPACE/.pylintrc + LINTER_RULES_PATH: $GITHUB_WORKSPACE + PYTHON_PYLINT_CONFIG_FILE: .pylintrc From 2aa4c9b488d8708bd0c8c01a53807be02f99a9ec Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 14:54:49 -0500 Subject: [PATCH 023/204] PAF-46 [no ci] --- .github/workflows/linter.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 009e828..81d8129 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -49,5 +49,4 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - LINTER_RULES_PATH: $GITHUB_WORKSPACE - PYTHON_PYLINT_CONFIG_FILE: .pylintrc + PYTHON_PYLINT_CONFIG_FILE: /.pylintrc From 2f4fb5bab5c03e79f6a77e8bbbab94916fe0bc0b Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 14:59:55 -0500 Subject: [PATCH 024/204] PAF-46 [skip ci] --- .github/workflows/linter.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 81d8129..48dcf57 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -49,4 +49,5 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PYTHON_PYLINT_CONFIG_FILE: /.pylintrc + LINTER_RULES_PATH: /github/workspace + PYTHON_PYLINT_CONFIG_FILE: .pylintrc From f979e4a65f3f9211835cb00df3ece0d990f3000a Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 11 Aug 2020 15:52:52 -0500 Subject: [PATCH 025/204] PAF-46 #Updating pylintrc [skip ci] --- .github/workflows/linter.yml | 3 +- .pylintrc | 598 +++++++++++++++++++- tests/special_test_settings_no_variables.py | 7 +- 3 files changed, 602 insertions(+), 6 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 48dcf57..1ca70eb 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -49,5 +49,4 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - LINTER_RULES_PATH: /github/workspace - PYTHON_PYLINT_CONFIG_FILE: .pylintrc + diff --git a/.pylintrc b/.pylintrc index 2e0f5c5..a865541 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1 +1,597 @@ -init-hook='from sys.path import append; from os import getcwd; append(getcwd())' \ No newline at end of file +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= +#init-hook='from sys.path import append; from os import getcwd; append(getcwd())' +init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/tests/special_test_settings_no_variables.py b/tests/special_test_settings_no_variables.py index d9fe603..d72c2db 100644 --- a/tests/special_test_settings_no_variables.py +++ b/tests/special_test_settings_no_variables.py @@ -1,3 +1,4 @@ +"""Special Cases for settings which requires user_variables.py to NOT exist""" import unittest from os import rename @@ -7,7 +8,7 @@ class TestSettingsWithoutVarFile(unittest.TestCase): def test_settings_without_var_file(self): """Test should return default value when user_variables missing""" rename("user_variables.py", "user_variables.py.tmp") - from dynatrace import settings - tz = settings.get_setting("DEFAULT_TIMEZONE") - self.assertEqual(tz, "UTC") + from dynatrace import settings # pylint: disable=import-outside-toplevel + timezone = settings.get_setting("DEFAULT_TIMEZONE") + self.assertEqual(timezone, "UTC") rename("user_variables.py.tmp", "user_variables.py") From e35c4435be2ff86d94ba8ce868eace55a2ed6d22 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 12 Aug 2020 09:31:24 -0500 Subject: [PATCH 026/204] PAF-46 # Another Attempt [skip ci] --- .github/workflows/linter.yml | 2 +- tests/test_settings.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 1ca70eb..8317869 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -48,5 +48,5 @@ jobs: env: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master + LINTER_RULES_PATH: . GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - diff --git a/tests/test_settings.py b/tests/test_settings.py index 5873f54..4f81d8e 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,17 +1,22 @@ +"""Test Cases for dynatrace.settings""" import unittest from dynatrace import settings class TestSettings(unittest.TestCase): + """Standard Testing Class""" def test_get_setting_from_user_variable(self): - tz = settings.get_setting("DEFAULT_TIMEZONE") - self.assertEqual(tz, "America/Chicago") + """Will pull from user_variables when available""" + timezone = settings.get_setting("DEFAULT_TIMEZONE") + self.assertEqual(timezone, "America/Chicago") def test_get_setting_from_default(self): + """When not in user_variables, info should be pulled from default values""" log_dir = settings.get_setting("LOG_DIR") self.assertEqual(log_dir, "logs/") def test_get_invalid_setting(self): + """"When not a valid setting, an error should be thrown""" with self.assertRaises(AttributeError) as context: settings.get_setting("INVALID_SETTING") From f48e0c01d1894c8f47d079da9c0edc05cf7af277 Mon Sep 17 00:00:00 2001 From: Aaron Date: Wed, 12 Aug 2020 10:18:51 -0500 Subject: [PATCH 027/204] PAF-46 #Flake8 Fixing and another attempt --- .github/workflows/linter.yml | 1 + tests/special_test_settings_no_variables.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 8317869..c48380d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -49,4 +49,5 @@ jobs: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master LINTER_RULES_PATH: . + PYTHON_PYLINT_CONFIG_FILE: .pylintrc GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/tests/special_test_settings_no_variables.py b/tests/special_test_settings_no_variables.py index d72c2db..e591020 100644 --- a/tests/special_test_settings_no_variables.py +++ b/tests/special_test_settings_no_variables.py @@ -8,7 +8,7 @@ class TestSettingsWithoutVarFile(unittest.TestCase): def test_settings_without_var_file(self): """Test should return default value when user_variables missing""" rename("user_variables.py", "user_variables.py.tmp") - from dynatrace import settings # pylint: disable=import-outside-toplevel + from dynatrace import settings # pylint: disable=import-outside-toplevel timezone = settings.get_setting("DEFAULT_TIMEZONE") self.assertEqual(timezone, "UTC") rename("user_variables.py.tmp", "user_variables.py") From a80339aaf06e88aba364322fca1e7de2ce1ff5a4 Mon Sep 17 00:00:00 2001 From: Philipose Date: Tue, 15 Sep 2020 22:50:38 -0400 Subject: [PATCH 028/204] updating pipfiles --- Pipfile.lock | 90 ++++++++++++++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 19a4db6..ad65880 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -68,52 +68,60 @@ "index": "pypi", "version": "==1.5.4" }, + "colorama": { + "hashes": [ + "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", + "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.4.3" + }, "coverage": { "hashes": [ - "sha256:098a703d913be6fbd146a8c50cc76513d726b022d170e5e98dc56d958fd592fb", - "sha256:16042dc7f8e632e0dcd5206a5095ebd18cb1d005f4c89694f7f8aafd96dd43a3", - "sha256:1adb6be0dcef0cf9434619d3b892772fdb48e793300f9d762e480e043bd8e716", - "sha256:27ca5a2bc04d68f0776f2cdcb8bbd508bbe430a7bf9c02315cd05fb1d86d0034", - "sha256:28f42dc5172ebdc32622a2c3f7ead1b836cdbf253569ae5673f499e35db0bac3", - "sha256:2fcc8b58953d74d199a1a4d633df8146f0ac36c4e720b4a1997e9b6327af43a8", - "sha256:304fbe451698373dc6653772c72c5d5e883a4aadaf20343592a7abb2e643dae0", - "sha256:30bc103587e0d3df9e52cd9da1dd915265a22fad0b72afe54daf840c984b564f", - "sha256:40f70f81be4d34f8d491e55936904db5c527b0711b2a46513641a5729783c2e4", - "sha256:4186fc95c9febeab5681bc3248553d5ec8c2999b8424d4fc3a39c9cba5796962", - "sha256:46794c815e56f1431c66d81943fa90721bb858375fb36e5903697d5eef88627d", - "sha256:4869ab1c1ed33953bb2433ce7b894a28d724b7aa76c19b11e2878034a4e4680b", - "sha256:4f6428b55d2916a69f8d6453e48a505c07b2245653b0aa9f0dee38785939f5e4", - "sha256:52f185ffd3291196dc1aae506b42e178a592b0b60a8610b108e6ad892cfc1bb3", - "sha256:538f2fd5eb64366f37c97fdb3077d665fa946d2b6d95447622292f38407f9258", - "sha256:64c4f340338c68c463f1b56e3f2f0423f7b17ba6c3febae80b81f0e093077f59", - "sha256:675192fca634f0df69af3493a48224f211f8db4e84452b08d5fcebb9167adb01", - "sha256:700997b77cfab016533b3e7dbc03b71d33ee4df1d79f2463a318ca0263fc29dd", - "sha256:8505e614c983834239f865da2dd336dcf9d72776b951d5dfa5ac36b987726e1b", - "sha256:962c44070c281d86398aeb8f64e1bf37816a4dfc6f4c0f114756b14fc575621d", - "sha256:9e536783a5acee79a9b308be97d3952b662748c4037b6a24cbb339dc7ed8eb89", - "sha256:9ea749fd447ce7fb1ac71f7616371f04054d969d412d37611716721931e36efd", - "sha256:a34cb28e0747ea15e82d13e14de606747e9e484fb28d63c999483f5d5188e89b", - "sha256:a3ee9c793ffefe2944d3a2bd928a0e436cd0ac2d9e3723152d6fd5398838ce7d", - "sha256:aab75d99f3f2874733946a7648ce87a50019eb90baef931698f96b76b6769a46", - "sha256:b1ed2bdb27b4c9fc87058a1cb751c4df8752002143ed393899edb82b131e0546", - "sha256:b360d8fd88d2bad01cb953d81fd2edd4be539df7bfec41e8753fe9f4456a5082", - "sha256:b8f58c7db64d8f27078cbf2a4391af6aa4e4767cc08b37555c4ae064b8558d9b", - "sha256:c1bbb628ed5192124889b51204de27c575b3ffc05a5a91307e7640eff1d48da4", - "sha256:c2ff24df02a125b7b346c4c9078c8936da06964cc2d276292c357d64378158f8", - "sha256:c890728a93fffd0407d7d37c1e6083ff3f9f211c83b4316fae3778417eab9811", - "sha256:c96472b8ca5dc135fb0aa62f79b033f02aa434fb03a8b190600a5ae4102df1fd", - "sha256:ce7866f29d3025b5b34c2e944e66ebef0d92e4a4f2463f7266daa03a1332a651", - "sha256:e26c993bd4b220429d4ec8c1468eca445a4064a61c74ca08da7429af9bc53bb0" + "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", + "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", + "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", + "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", + "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", + "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", + "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", + "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", + "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", + "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", + "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", + "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", + "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", + "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", + "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", + "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", + "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", + "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", + "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", + "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", + "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", + "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", + "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", + "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", + "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", + "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", + "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", + "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", + "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", + "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", + "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", + "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", + "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", + "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" ], "index": "pypi", - "version": "==5.2.1" + "version": "==5.3" }, "isort": { "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + "sha256:171c5f365791073426b5ed3a156c2081a47f88c329161fd28228ff2da4c97ddb", + "sha256:ba91218eee31f1e300ecc079ef0c524cea3fc41bfbb979cbdf5fd3a889e3cfed" ], - "version": "==4.3.21" + "version": "==5.5.2" }, "lazy-object-proxy": { "hashes": [ @@ -157,11 +165,11 @@ }, "pylint": { "hashes": [ - "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", - "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" + "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", + "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" ], "index": "pypi", - "version": "==2.5.3" + "version": "==2.6.0" }, "six": { "hashes": [ From d078aaa8a499d101668f3bb3250c03583fde7195 Mon Sep 17 00:00:00 2001 From: Philipose Date: Tue, 15 Sep 2020 23:28:26 -0400 Subject: [PATCH 029/204] renaming SSL to stop confusion in linter --- dynatrace/cluster/{ssl.py => ssl_cert.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dynatrace/cluster/{ssl.py => ssl_cert.py} (100%) diff --git a/dynatrace/cluster/ssl.py b/dynatrace/cluster/ssl_cert.py similarity index 100% rename from dynatrace/cluster/ssl.py rename to dynatrace/cluster/ssl_cert.py From 716ec3c8dcb461622518e942e2e3e1a75b46711e Mon Sep 17 00:00:00 2001 From: Aaron Date: Thu, 17 Sep 2020 20:46:46 -0400 Subject: [PATCH 030/204] PAF-46 #Linting Adjustments --- dynatrace/requests/request_handler.py | 21 ++++++++++++--------- dynatrace/tenant/maintenance.py | 24 ++++++++++++------------ tests/tooling_for_test.py | 15 +++++++++++++-- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 3baf04d..4b4afa0 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -1,10 +1,11 @@ """Make API Request to available Dynatrace API""" -import requests +from enum import Enum, auto import time +import requests from dynatrace.exceptions import InvalidAPIResponseException, ManagedClusterOnlyException -from enum import Enum, auto -requests.packages.urllib3.disable_warnings() + +requests.packages.urllib3.disable_warnings() # pylint: disable=no-member HTTPS_STR = "https://" @@ -25,7 +26,7 @@ class ClusterAPIs(Enum): USERS = f"{BASE}/users" def __str__(self): - return self.value + return str(self.value) class TenantAPIs(Enum): @@ -60,7 +61,7 @@ class TenantAPIs(Enum): REQUEST_NAMING = "/api/config/v1/service/requestNaming" def __str__(self): - return self.value + return str(self.value) class HTTP(Enum): @@ -74,16 +75,17 @@ class HTTP(Enum): DELETE = auto() def __str__(self): - return self.name + return str(self.name) def __repr__(self): - return self.name + return str(self.name) def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method=HTTP.GET): ''' Function makes an API call in a safe way, taking into account the rate limits. - This will ensure the API call will always go through, with the program waiting for the limit to reset if needed.\n + This will ensure the API call will always go through.\n + The program will wait for the limit to reset if needed.\n @param cluster - Cluster dictionary from variable_set\n @param endpoint - API endpoint to call.\n @@ -94,7 +96,8 @@ def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method @return - response from request\n ''' # Set the right URL for the operation - url = f"{generate_tenant_url(cluster, tenant)}{endpoint}" if tenant else f"{HTTPS_STR}{cluster['url']}" + url = f"{generate_tenant_url(cluster, tenant)}{endpoint}" \ + if tenant else f"{HTTPS_STR}{cluster['url']}" if not params: params = {} diff --git a/dynatrace/tenant/maintenance.py b/dynatrace/tenant/maintenance.py index e40ddb3..5d44cb4 100644 --- a/dynatrace/tenant/maintenance.py +++ b/dynatrace/tenant/maintenance.py @@ -1,10 +1,10 @@ """Maintenance Window Operations""" import datetime import re +from enum import Enum, auto import dynatrace.requests.request_handler as rh import user_variables as uv from dynatrace.exceptions import InvalidDateFormatException -from enum import Enum, auto MZ_ENDPOINT = rh.TenantAPIs.MAINTENANCE_WINDOWS @@ -16,7 +16,7 @@ class Suppression(Enum): Args: Enum (DETECT_PROBLEMS_AND_ALERT): Full Detection and Alerting during Maintenance Window - Enum (DETECT_PROBLEMS_DONT_ALERT): Problems detected but alerting profiles in that scope are not triggered + Enum (DETECT_PROBLEMS_DONT_ALERT): Problems detected but alerts in that scope are not triggered Enum (DONT_DETECT_PROBLEMS): Problem detection completely off for the scope """ DETECT_PROBLEMS_AND_ALERT = auto() @@ -24,10 +24,10 @@ class Suppression(Enum): DONT_DETECT_PROBLEMS = auto() def __str__(self): - return self.name + return str(self.name) def __repr__(self): - return self.name + return str(self.name) class DayOfWeek(Enum): @@ -53,10 +53,10 @@ class DayOfWeek(Enum): SUNDAY = auto() def __str__(self): - return self.name + return str(self.name) def __repr__(self): - return self.name + return str(self.name) class Context(Enum): @@ -71,10 +71,10 @@ class Context(Enum): KUBERNETES = auto() def __str__(self): - return self.name + return str(self.name) def __repr__(self): - return self.name + return str(self.name) class RecurrenceType(Enum): @@ -85,10 +85,10 @@ class RecurrenceType(Enum): WEEKLY = auto() def __str__(self): - return self.name + return str(self.name) def __repr__(self): - return self.name + return str(self.name) class FilterType(Enum): @@ -187,10 +187,10 @@ class FilterType(Enum): VMWARE_DATACENTER = auto() def __str__(self): - return self.name + return str(self.name) def __repr__(self): - return self.name + return str(self.name) def validate_datetime(datetime_text, required_format): diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index 27e14fb..d1ad09f 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -1,14 +1,25 @@ """Mockserver Expectation Setup""" -import requests import json import logging +import requests from dynatrace.requests.request_handler import generate_tenant_url logging.basicConfig(filename="testing_tools.log", level=logging.DEBUG) def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwargs): - requests.packages.urllib3.disable_warnings() + """Create Payload For MockServer to expect and respond + + Args: + cluster (Dictionary): [description] + tenant (str): [description] + url_path (str): [description] + request_type (HTTP str): [description] + + Raises: + ValueError: [description] + """ + requests.packages.urllib3.disable_warnings() # pylint: disable=no-member expectation = { "httpRequest": { "headers": { From 43707a04a6501d92a04a7e5d3f8db911187e60fd Mon Sep 17 00:00:00 2001 From: Aaron Date: Fri, 18 Sep 2020 12:58:14 -0400 Subject: [PATCH 031/204] PAF-46 Adding Flake8 to Dev Packages --- Pipfile | 1 + Pipfile.lock | 33 ++++++++++++++++++++++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/Pipfile b/Pipfile index 393ec6b..897d6db 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ verify_ssl = true pylint = "*" autopep8 = "*" coverage = "*" +flake8 = "*" [packages] requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index ad65880..ddbc73a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "d77ab23630511fa40710f418270d79d24bc9d2b8a61ab2d2af4b0e938036b609" + "sha256": "821caf8bcd1fbece0d9ec4853bfb6fd206066401b8eceb209b95e7f77629dc2b" }, "pipfile-spec": 6, "requires": { @@ -35,6 +35,7 @@ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, "requests": { @@ -50,6 +51,7 @@ "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "version": "==1.25.10" } }, @@ -59,6 +61,7 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], + "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "autopep8": { @@ -68,14 +71,6 @@ "index": "pypi", "version": "==1.5.4" }, - "colorama": { - "hashes": [ - "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", - "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1" - ], - "markers": "sys_platform == 'win32'", - "version": "==0.4.3" - }, "coverage": { "hashes": [ "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", @@ -116,11 +111,20 @@ "index": "pypi", "version": "==5.3" }, + "flake8": { + "hashes": [ + "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", + "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" + ], + "index": "pypi", + "version": "==3.8.3" + }, "isort": { "hashes": [ "sha256:171c5f365791073426b5ed3a156c2081a47f88c329161fd28228ff2da4c97ddb", "sha256:ba91218eee31f1e300ecc079ef0c524cea3fc41bfbb979cbdf5fd3a889e3cfed" ], + "markers": "python_version >= '3.6' and python_version < '4.0'", "version": "==5.5.2" }, "lazy-object-proxy": { @@ -147,6 +151,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -161,8 +166,17 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, + "pyflakes": { + "hashes": [ + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.2.0" + }, "pylint": { "hashes": [ "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", @@ -176,6 +190,7 @@ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "toml": { From b68eb85a41f81db080fc1ece6da9197ecadc4004 Mon Sep 17 00:00:00 2001 From: Aaron Date: Fri, 18 Sep 2020 12:59:01 -0400 Subject: [PATCH 032/204] PAF-46 #Add linting suggestions --- .gitignore | 1 + dynatrace/requests/request_handler.py | 2 +- dynatrace/tenant/maintenance.py | 84 +++++++++++++++++++++------ tests/test_maintenance_windows.py | 2 +- 4 files changed, 69 insertions(+), 20 deletions(-) diff --git a/.gitignore b/.gitignore index 3465291..852b2a8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ templates/* **venv** # Framework log files ***.log** +***.tap** user_variables.py sandbox_script.py diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 4b4afa0..42f8bc1 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -97,7 +97,7 @@ def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method ''' # Set the right URL for the operation url = f"{generate_tenant_url(cluster, tenant)}{endpoint}" \ - if tenant else f"{HTTPS_STR}{cluster['url']}" + if tenant else f"{HTTPS_STR}{cluster['url']}" if not params: params = {} diff --git a/dynatrace/tenant/maintenance.py b/dynatrace/tenant/maintenance.py index 5d44cb4..9aae7a2 100644 --- a/dynatrace/tenant/maintenance.py +++ b/dynatrace/tenant/maintenance.py @@ -3,7 +3,7 @@ import re from enum import Enum, auto import dynatrace.requests.request_handler as rh -import user_variables as uv +import user_variables from dynatrace.exceptions import InvalidDateFormatException @@ -16,7 +16,7 @@ class Suppression(Enum): Args: Enum (DETECT_PROBLEMS_AND_ALERT): Full Detection and Alerting during Maintenance Window - Enum (DETECT_PROBLEMS_DONT_ALERT): Problems detected but alerts in that scope are not triggered + Enum (DETECT_PROBLEMS_DONT_ALERT): Problems detected but alerts in scope are not triggered Enum (DONT_DETECT_PROBLEMS): Problem detection completely off for the scope """ DETECT_PROBLEMS_AND_ALERT = auto() @@ -194,20 +194,42 @@ def __repr__(self): def validate_datetime(datetime_text, required_format): + """Validate input against expected DateTime format + + Args: + datetime_text (str): Time inputted + required_format (str): Expected format to validate against + + Raises: + InvalidDateFormatException: Used for incorrect format provided + """ try: datetime.datetime.strptime(datetime_text, required_format) except ValueError: - raise InvalidDateFormatException(required_format) + raise InvalidDateFormatException(required_format) from ValueError def generate_tag_scope(tag, filter_type=None, management_zone_id=None): + """Generating Tag portion of scope + + Args: + tag (list, dict, str): single or collection of tags + filter_type (str, optional): Type of entity to match against. Defaults to None. + management_zone_id (str, optional): Management Zone to match against. Defaults to None. + + Raises: + ValueError: Filter Type is not in acceptable values + + Returns: + dict: tag payload to be used as part of the main scope payload + """ tag_payload = {} if management_zone_id: tag_payload['mzId'] = str(management_zone_id) if filter_type: - if filter_type in FilterType._member_names_: + if filter_type in FilterType._member_names_: # pylint: disable=no-member,protected-access tag_payload['type'] = filter_type else: raise ValueError( @@ -225,7 +247,25 @@ def generate_tag_scope(tag, filter_type=None, management_zone_id=None): return tag_payload -def generate_scope(entities=None, tags=None, filter_type=None, management_zone_id=None, match_any_tag=True): +def generate_scope( + entities=None, + tags=None, + filter_type=None, + management_zone_id=None, + match_any_tag=True + ): + """Generate the total scope for maintenance window payload + + Args: + entities (list, optional): List of specific entities. Defaults to None. + tags (List,Dict,str, optional): List/Set/Individual Tags. Defaults to None. + filter_type (str, optional): Specific Entity Type for tag. Defaults to None. + management_zone_id ([type], optional): Specific MZ for tag. Defaults to None. + match_any_tag (bool, optional): Any vs All. Defaults to True. + + Returns: + dict: sub payload for maintenance window payload containing scope + """ if entities is None: entities = [] matches = [] @@ -269,13 +309,21 @@ def generate_window_json(name, description, suppression, schedule, scope=None, i return window_json -def generate_schedule(recurrence_type, start_time, duration, range_start, range_end, day=None, zoneId=None,): +def generate_schedule( + recurrence_type, + start_time, + duration, + range_start, + range_end, + day=None, + zone_id=None, + ): """Create schedule structure for maintenance window""" # This structure requires a lot of input validation recurrence_type = str(recurrence_type).upper() # Check Recurrence - if recurrence_type not in RecurrenceType._member_names_: + if recurrence_type not in RecurrenceType._member_names_: # pylint: disable=no-member,protected-access raise ValueError( "Invalid Recurrence Type! Allowed values are: ONCE, DAILY, WEEKLY, MONTHLY") @@ -289,8 +337,8 @@ def generate_schedule(recurrence_type, start_time, duration, range_start, range_ "end": range_end } - if zoneId is None: - schedule['zoneId'] = uv.DEFAULT_TIMEZONE + if zone_id is None: + schedule['zoneId'] = user_variables.DEFAULT_TIMEZONE if recurrence_type != "ONCE": # Check Start Time @@ -300,7 +348,7 @@ def generate_schedule(recurrence_type, start_time, duration, range_start, range_ try: int(duration) except ValueError: - ("Duration time must be an integer! Duration is length of Maintainence Window in minutes") + print ("Duration time must be integer! Duration of Maintainence Window in minutes") schedule['recurrence'] = { "startTime": start_time, @@ -310,7 +358,7 @@ def generate_schedule(recurrence_type, start_time, duration, range_start, range_ # Check Weekly Day if recurrence_type == "WEEKLY": day = str(day).upper() - if day in DayOfWeek._member_names_: + if day in DayOfWeek._member_names_: # pylint: disable=no-member,protected-access schedule['recurrence']['dayOfWeek'] = day else: raise ValueError("Invalid Weekly Day! Allowed values are " @@ -320,7 +368,7 @@ def generate_schedule(recurrence_type, start_time, duration, range_start, range_ if recurrence_type == "MONTHLY": if not isinstance(day, int): raise TypeError("Invalid type for Day of Month! Int between 1-31 required") - if (1 <= int(day) <= 31): + if 1 <= int(day) <= 31: schedule['recurrence']['dayOfMonth'] = day else: raise ValueError("Invalid Monthly Day! Allowed values are 1-31") @@ -376,19 +424,19 @@ def get_window(cluster, tenant, window_id): def parse_tag(tag_string): # Need a way to process literal colon inside a key "Parsing Tag to to Context, Key and Value" - m = re.match( + tag_match = re.match( r"(?:\[(\w+)\])?([\w\-\/`\+\.\!\@\#\$\%\^\&\*\(\)\?\[\]\{\}\,\<\>\ \:\;]+)(?:\:(\w*))?", tag_string ) tag_dictionary = {} - if m.group(1): - tag_dictionary['context'] = m.group(1) + if tag_match.group(1): + tag_dictionary['context'] = tag_match.group(1) else: tag_dictionary['context'] = "CONTEXTLESS" - tag_dictionary['key'] = m.group(2) # Key is always required + tag_dictionary['key'] = tag_match.group(2) # Key is always required - if m.group(3): - tag_dictionary['value'] = m.group(3) + if tag_match.group(3): + tag_dictionary['value'] = tag_match.group(3) return tag_dictionary diff --git a/tests/test_maintenance_windows.py b/tests/test_maintenance_windows.py index 34262f2..3549500 100644 --- a/tests/test_maintenance_windows.py +++ b/tests/test_maintenance_windows.py @@ -363,7 +363,7 @@ def test_invalid_filter_type(self): filter_type="INVALID_TYPE" ) self.assertTrue("Invalid Filter Type" in ( - msg := str(context.exception)), msg) + msg := str(context.exception)), msg) # pylint: disable=used-before-assignment class TestMaintenanceEnumTypes(unittest.TestCase): From edb35fb78bd24fb4c57dfd5749ffe0787e359d31 Mon Sep 17 00:00:00 2001 From: Aaron Date: Fri, 18 Sep 2020 13:59:26 -0400 Subject: [PATCH 033/204] PAF-46 #Linting Adjustments, Readme update --- README.md | 4 ++-- dynatrace/settings.py | 13 +++++++++++++ tests/test_topology_processes.py | 3 ++- tests/tooling_for_test.py | 24 ++++++++++++++++-------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 187f8c9..da78415 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Dynatrace Python3 API Framework -Testing the "Test" Branch +![Lint Code Base](https://github.com/dynatrace-api-framework/python3-framework/workflows/Lint%20Code%20Base/badge.svg) **Python Requirements**: Requests -Python >= 3.4 (Built and tested with Python 3.8) +Python >= 3.8 **How To Use** diff --git a/dynatrace/settings.py b/dynatrace/settings.py index d96386c..8fdd032 100644 --- a/dynatrace/settings.py +++ b/dynatrace/settings.py @@ -1,3 +1,4 @@ +"""Module for retreiving settings safely""" try: import user_variables FILE_IMPORTED = True @@ -6,6 +7,7 @@ class DefaultSettings(): + """Default settings if not specified elsewhere""" LOG_LEVEL = None LOG_DIR = "logs/" @@ -34,6 +36,17 @@ class DefaultSettings(): def get_setting(attribute): + """Fetch setting from user-defined files or else default values + + Args: + attribute (str): attribute/setting to retreive + + Raises: + AttributeError: Setting not defaulted nor user-defined + + Returns: + [type]: attribute in it's correct variable type if found + """ if FILE_IMPORTED and hasattr(user_variables, attribute): return getattr(user_variables, attribute) elif hasattr(DefaultSettings, attribute): diff --git a/tests/test_topology_processes.py b/tests/test_topology_processes.py index 3fc29b1..e9c48a9 100644 --- a/tests/test_topology_processes.py +++ b/tests/test_topology_processes.py @@ -1,11 +1,12 @@ """Test suite for Topology Processes""" import unittest -from user_variables import FULL_SET from tests import tooling_for_test as testtools +from dynatrace import settings from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import process +FULL_SET = settings.get_setting("FULL_SET") CLUSTER = FULL_SET.get('mockserver1') TENANT = 'tenant1' URL_PATH = f"{TenantAPIs.V1_TOPOLOGY}/infrastructure/processes" diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index d1ad09f..21f6cbb 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -38,23 +38,23 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa "id": "OneOff", } - logging.debug(f"URL PATH: {url_path}") - logging.debug(f"KWARGS {kwargs}") + logging.debug("URL PATH: %s", url_path) + logging.debug("KWARGS %s", kwargs) # Paramaters should always at least have Api-Token if 'parameters' in kwargs: expectation["httpRequest"]["queryStringParameters"] = kwargs['parameters'] if "request_file" in kwargs: - with open(kwargs['request_file']) as f: - request_payload = json.load(f) + with open(kwargs['request_file']) as open_file: + request_payload = json.load(open_file) expectation["httpRequest"]["body"] = { "type": "JSON", "json": request_payload, } if "response_file" in kwargs: - with open(kwargs['response_file']) as f: - response_payload = json.load(f) + with open(kwargs['response_file']) as open_file: + response_payload = json.load(open_file) expectation["httpResponse"]["body"] = { "type": "JSON", "json": response_payload, @@ -85,5 +85,13 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa def expected_payload(json_file): - with open(json_file) as f: - return json.load(f) + """The payload that should be tested against + + Args: + json_file (str): file name for result json + + Returns: + dict: payload of the expected result JSON + """ + with open(json_file) as open_file: + return json.load(open_file) From 145fd38d2dc8e4c395cb2c096ccdcdc5154827ad Mon Sep 17 00:00:00 2001 From: Philipose Date: Wed, 23 Sep 2020 11:56:04 -0400 Subject: [PATCH 034/204] PAF-46 #Lint Fixes --- dynatrace/exceptions.py | 8 ++++++++ dynatrace/requests/request_handler.py | 8 +++++--- dynatrace/tenant/extensions.py | 1 + dynatrace/tenant/host_groups.py | 25 +++++++++++++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/dynatrace/exceptions.py b/dynatrace/exceptions.py index 25ed096..a4932fe 100644 --- a/dynatrace/exceptions.py +++ b/dynatrace/exceptions.py @@ -5,22 +5,30 @@ class InvalidAPIResponseException (Exception): + """The framework did not get an expected result from the Dynatrace API""" def __init__(self, message): + super(InvalidAPIResponseException, Exception).__init__(message) print(message, file=stderr) class InvalidDateFormatException(ValueError): + """The Date provided does not match the format needed""" def __init__(self, required_format): + super(InvalidDateFormatException, ValueError).__init__(required_format) self.message = f"Incorrect Date for following entry: {required_format}" class InvalidScopeException(ValueError): + """The Scope is incomplete or misconfigured""" def __init__(self, required_format): + super(InvalidScopeException, ValueError).__init__(required_format) self.required_format = required_format print("Invalid scope used. Tag required for management zone, matching rule: %s", required_format, file=stderr) class ManagedClusterOnlyException(TypeError): + """The operation is only supported on a managed cluster""" def __init__(self): + super(ManagedClusterOnlyException, TypeError).__init__() print("This operation is only supported on Dynatrace Managed!", file=stderr) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 42f8bc1..68e1251 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -39,7 +39,8 @@ class TenantAPIs(Enum): PROBLEM_STATUS = "/api/v1/problem/status" DEPLOY_ONEAGENT = "/api/v1/deployment/installer/agent" DEPLOY_ONEAGENT_CONNECTION_INFO = "/api/v1/deployment/installer/agent/connectioninfo" - DEPLOY_ONEAGENT_CONNECTION_ENDPOINTS = "/api/v1/deployment/installer/agent/connectioninfo/endpoints" + DEPLOY_ONEAGENT_CONNECTION_ENDPOINTS = \ + "/api/v1/deployment/installer/agent/connectioninfo/endpoints" DEPLOY_ACTIVEGATE = "/api/v1/deployment/installer/gateway" DEPLOY_BOSH = "/api/v1/deployment/boshrelease" EVENTS = "/api/v1/events" @@ -97,7 +98,7 @@ def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method ''' # Set the right URL for the operation url = f"{generate_tenant_url(cluster, tenant)}{endpoint}" \ - if tenant else f"{HTTPS_STR}{cluster['url']}" + if tenant else f"{HTTPS_STR}{cluster['url']}{endpoint}" if not params: params = {} @@ -146,7 +147,8 @@ def check_response(response): time.sleep(float(time_to_wait)) return False elif not 200 <= response.status_code <= 299: - raise InvalidAPIResponseException(f"Response Error:\n{response.url}\n{response.status_code}\n{response.text}") + raise InvalidAPIResponseException(\ + f"Response Error:\n{response.url}\n{response.status_code}\n{response.text}") return True diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index a5967e8..db43fb7 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -1,3 +1,4 @@ +"""Operations Interacting with Dynatrace Extensions""" from dynatrace.requests import request_handler as rh ENDPOINT = rh.TenantAPIs.EXTENSIONS diff --git a/dynatrace/tenant/host_groups.py b/dynatrace/tenant/host_groups.py index 495b012..968acff 100644 --- a/dynatrace/tenant/host_groups.py +++ b/dynatrace/tenant/host_groups.py @@ -12,6 +12,15 @@ def get_host_groups_tenantwide(cluster, tenant): + """Get all Host Groups in the Tenant + + Args: + cluster (Cluster Dict): Dictionary containing all Cluster info + tenant (str): String with the tenant name that is being selected + + Returns: + Dict: List of Host Groups in the tenant + """ params = { 'relativeTime': 'day', 'includeDetails': 'true' @@ -27,6 +36,14 @@ def get_host_groups_tenantwide(cluster, tenant): def get_host_groups_clusterwide(cluster): + """Get all Host Groups used in the Cluster + + Args: + cluster (cluster dict): Current cluster to operate on + + Returns: + dict: Dictionary of all Host Groups used in the Cluster + """ # TODO add split_by_tenant optional variable host_groups_custerwide = {} for tenant in cluster['tenant']: @@ -38,6 +55,14 @@ def get_host_groups_clusterwide(cluster): def get_host_groups_setwide(full_set): # TODO add split_by_tenant optional variable + """Get all Host Groups used in the Cluster Set + + Args: + full_set (dict of cluster dict): Current cluster to operate on + + Returns: + dict: Dictionary of all Host Groups used in the Cluster Set + """ host_groups_setwide = {} for cluster in full_set.values(): host_groups_setwide.update(get_host_groups_clusterwide(cluster)) From bd879e95da4da7690152b28a3bfe652b3fedfb11 Mon Sep 17 00:00:00 2001 From: Philipose Date: Wed, 23 Sep 2020 12:01:19 -0400 Subject: [PATCH 035/204] Fixing Endpoint for SSO --- dynatrace/cluster/sso.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/cluster/sso.py b/dynatrace/cluster/sso.py index df73b0a..885c28e 100644 --- a/dynatrace/cluster/sso.py +++ b/dynatrace/cluster/sso.py @@ -1,7 +1,7 @@ """SSO Operations for Dynatrace""" import dynatrace.requests.request_handler as rh -ENDPOINT = "sso/ssoProvider" +ENDPOINT = "/api/v1.0/onpremise/sso/ssoProvider" def disable_sso(cluster): From a2190267e139feba9cf8758398ccf596c8285b2a Mon Sep 17 00:00:00 2001 From: Philipose Date: Wed, 23 Sep 2020 13:30:44 -0400 Subject: [PATCH 036/204] PAF-46 #Testing Exception break, linting --- dynatrace/cluster/sso.py | 8 ++++++++ dynatrace/exceptions.py | 6 +++--- tests/test_maintenance_windows.py | 4 ++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/dynatrace/cluster/sso.py b/dynatrace/cluster/sso.py index 885c28e..ab881ed 100644 --- a/dynatrace/cluster/sso.py +++ b/dynatrace/cluster/sso.py @@ -45,6 +45,14 @@ def enable_sso(cluster, disable_local=False, groups_enabled=False, is_openid=Fal def get_sso_status(cluster): + """Current Settings of SSO + + Args: + cluster (cluster dict): Currently selected cluster + + Returns: + dict: All SSO settings in their current setting + """ response = rh.make_api_call(cluster=cluster, endpoint=ENDPOINT) return response.json() diff --git a/dynatrace/exceptions.py b/dynatrace/exceptions.py index a4932fe..2d09e18 100644 --- a/dynatrace/exceptions.py +++ b/dynatrace/exceptions.py @@ -7,21 +7,21 @@ class InvalidAPIResponseException (Exception): """The framework did not get an expected result from the Dynatrace API""" def __init__(self, message): - super(InvalidAPIResponseException, Exception).__init__(message) + super(InvalidAPIResponseException, self).__init__(message) print(message, file=stderr) class InvalidDateFormatException(ValueError): """The Date provided does not match the format needed""" def __init__(self, required_format): - super(InvalidDateFormatException, ValueError).__init__(required_format) + super(InvalidDateFormatException, self).__init__(required_format) self.message = f"Incorrect Date for following entry: {required_format}" class InvalidScopeException(ValueError): """The Scope is incomplete or misconfigured""" def __init__(self, required_format): - super(InvalidScopeException, ValueError).__init__(required_format) + super(InvalidScopeException, self).__init__(required_format) self.required_format = required_format print("Invalid scope used. Tag required for management zone, matching rule: %s", required_format, file=stderr) diff --git a/tests/test_maintenance_windows.py b/tests/test_maintenance_windows.py index 3549500..4d9a63c 100644 --- a/tests/test_maintenance_windows.py +++ b/tests/test_maintenance_windows.py @@ -263,6 +263,7 @@ def test_create_monthly_no_scope(self): class TestMaintenanceExceptions(unittest.TestCase): + """Series of Tests aimed at triggering exception""" def test_invalid_recurrence_type(self): """Testing exception thrown for invalid recurrence type""" with self.assertRaises(ValueError) as context: @@ -367,13 +368,16 @@ def test_invalid_filter_type(self): class TestMaintenanceEnumTypes(unittest.TestCase): + """Test to validate Maintenance Enum Types are correct""" def test_suppression_enum_str(self): + """Suppression enum should be string""" suppression = maintenance.Suppression( maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) self.assertIsInstance( maintenance.Suppression.__str__(suppression), str) def test_suppression_enum_repr(self): + """Suppression enum should be string""" suppression = maintenance.Suppression( maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) self.assertIsInstance( From 200a3ac7a42f3beb26438721f5b399ae81c5f994 Mon Sep 17 00:00:00 2001 From: Philipose Date: Wed, 23 Sep 2020 13:40:23 -0400 Subject: [PATCH 037/204] PAF-46 #Flake8 Linting --- dynatrace/requests/request_handler.py | 4 ++-- dynatrace/tenant/maintenance.py | 30 +++++++++++++-------------- tests/test_maintenance_windows.py | 2 +- tests/tooling_for_test.py | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 68e1251..5e8fe57 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -5,7 +5,7 @@ from dynatrace.exceptions import InvalidAPIResponseException, ManagedClusterOnlyException -requests.packages.urllib3.disable_warnings() # pylint: disable=no-member +requests.packages.urllib3.disable_warnings() # pylint: disable=no-member HTTPS_STR = "https://" @@ -147,7 +147,7 @@ def check_response(response): time.sleep(float(time_to_wait)) return False elif not 200 <= response.status_code <= 299: - raise InvalidAPIResponseException(\ + raise InvalidAPIResponseException( f"Response Error:\n{response.url}\n{response.status_code}\n{response.text}") return True diff --git a/dynatrace/tenant/maintenance.py b/dynatrace/tenant/maintenance.py index 9aae7a2..09681bd 100644 --- a/dynatrace/tenant/maintenance.py +++ b/dynatrace/tenant/maintenance.py @@ -248,12 +248,12 @@ def generate_tag_scope(tag, filter_type=None, management_zone_id=None): def generate_scope( - entities=None, - tags=None, - filter_type=None, - management_zone_id=None, - match_any_tag=True - ): + entities=None, + tags=None, + filter_type=None, + management_zone_id=None, + match_any_tag=True +): """Generate the total scope for maintenance window payload Args: @@ -310,14 +310,14 @@ def generate_window_json(name, description, suppression, schedule, scope=None, i def generate_schedule( - recurrence_type, - start_time, - duration, - range_start, - range_end, - day=None, - zone_id=None, - ): + recurrence_type, + start_time, + duration, + range_start, + range_end, + day=None, + zone_id=None, +): """Create schedule structure for maintenance window""" # This structure requires a lot of input validation recurrence_type = str(recurrence_type).upper() @@ -348,7 +348,7 @@ def generate_schedule( try: int(duration) except ValueError: - print ("Duration time must be integer! Duration of Maintainence Window in minutes") + print("Duration time must be integer! Duration of Maintainence Window in minutes") schedule['recurrence'] = { "startTime": start_time, diff --git a/tests/test_maintenance_windows.py b/tests/test_maintenance_windows.py index 4d9a63c..26e33f4 100644 --- a/tests/test_maintenance_windows.py +++ b/tests/test_maintenance_windows.py @@ -364,7 +364,7 @@ def test_invalid_filter_type(self): filter_type="INVALID_TYPE" ) self.assertTrue("Invalid Filter Type" in ( - msg := str(context.exception)), msg) # pylint: disable=used-before-assignment + msg := str(context.exception)), msg) # pylint: disable=used-before-assignment class TestMaintenanceEnumTypes(unittest.TestCase): diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index 21f6cbb..63b2275 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -19,7 +19,7 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa Raises: ValueError: [description] """ - requests.packages.urllib3.disable_warnings() # pylint: disable=no-member + requests.packages.urllib3.disable_warnings() # pylint: disable=no-member expectation = { "httpRequest": { "headers": { From 31e9812731dfd9d2ab7944306cde543db7da25e5 Mon Sep 17 00:00:00 2001 From: Philipose Date: Wed, 23 Sep 2020 23:40:00 -0400 Subject: [PATCH 038/204] PAF-46 #Adjusting GitHub Actions [no-ci] --- .github/workflows/linter.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index c48380d..76d0e68 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -51,3 +51,8 @@ jobs: LINTER_RULES_PATH: . PYTHON_PYLINT_CONFIG_FILE: .pylintrc GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Archive code coverage results + uses: actions/upload-artifact@v2 + with: + name: linter-report + path: super-linter.log From b8e566d41729414ce70ac73254bf0a55783e74cc Mon Sep 17 00:00:00 2001 From: Philipose Date: Wed, 23 Sep 2020 23:47:53 -0400 Subject: [PATCH 039/204] PAF-46 #Specifying LOG ENV var --- .github/workflows/linter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 76d0e68..a0e6807 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -51,6 +51,7 @@ jobs: LINTER_RULES_PATH: . PYTHON_PYLINT_CONFIG_FILE: .pylintrc GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LOG_FILE: super-linter.log - name: Archive code coverage results uses: actions/upload-artifact@v2 with: From 989ebf6f792327f9392358b15e6a8908ea0c4eb9 Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 00:02:01 -0400 Subject: [PATCH 040/204] PAF-46 #Linting and trying to get lint log --- .github/workflows/linter.yml | 2 +- tests/test_maintenance_windows.py | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index a0e6807..86b0c8d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -52,7 +52,7 @@ jobs: PYTHON_PYLINT_CONFIG_FILE: .pylintrc GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LOG_FILE: super-linter.log - - name: Archive code coverage results + - name: Archive super-linter log uses: actions/upload-artifact@v2 with: name: linter-report diff --git a/tests/test_maintenance_windows.py b/tests/test_maintenance_windows.py index 26e33f4..75942a6 100644 --- a/tests/test_maintenance_windows.py +++ b/tests/test_maintenance_windows.py @@ -370,53 +370,61 @@ def test_invalid_filter_type(self): class TestMaintenanceEnumTypes(unittest.TestCase): """Test to validate Maintenance Enum Types are correct""" def test_suppression_enum_str(self): - """Suppression enum should be string""" + """Suppression enum str should be string""" suppression = maintenance.Suppression( maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) self.assertIsInstance( maintenance.Suppression.__str__(suppression), str) def test_suppression_enum_repr(self): - """Suppression enum should be string""" + """Suppression enum repr should be string""" suppression = maintenance.Suppression( maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) self.assertIsInstance( maintenance.Suppression.__repr__(suppression), str) def test_day_of_week_enum_str(self): + """Day of Week enum str should be string""" day_of_week = maintenance.DayOfWeek(maintenance.DayOfWeek.MONDAY) self.assertIsInstance(maintenance.DayOfWeek.__str__(day_of_week), str) def test_day_of_week_enum_repr(self): + """Day of Week enum repr should be string""" day_of_week = maintenance.DayOfWeek(maintenance.DayOfWeek.MONDAY) self.assertIsInstance(maintenance.DayOfWeek.__repr__(day_of_week), str) def test_context_enum_str(self): + """Context enum str should be string""" context = maintenance.Context(maintenance.Context.CONTEXTLESS) self.assertIsInstance(maintenance.Context.__str__(context), str) def test_context_enum_repr(self): + """Context enum repr should be string""" context = maintenance.Context(maintenance.Context.CONTEXTLESS) self.assertIsInstance(maintenance.Context.__repr__(context), str) def test_recurrence_type_enum_str(self): + """Recurrence Type enum str should be string""" recurrence_type = maintenance.RecurrenceType( maintenance.RecurrenceType.DAILY) self.assertIsInstance( maintenance.RecurrenceType.__str__(recurrence_type), str) def test_recurrence_type_enum_repr(self): + """Recurrence Type enum repr should be string""" recurrence_type = maintenance.RecurrenceType( maintenance.RecurrenceType.DAILY) self.assertIsInstance( maintenance.RecurrenceType.__repr__(recurrence_type), str) def test_filter_type_enum_str(self): + """Filter Type enum str should be string""" suppression = maintenance.FilterType( maintenance.FilterType.APM_SECURITY_GATEWAY) self.assertIsInstance(maintenance.FilterType.__str__(suppression), str) def test_filter_type_enum_repr(self): + """Filter Type enum repr should be string""" suppression = maintenance.FilterType( maintenance.FilterType.APM_SECURITY_GATEWAY) self.assertIsInstance( @@ -424,6 +432,7 @@ def test_filter_type_enum_repr(self): class TestTagParsing(unittest.TestCase): + """Testing Maintenance Window Tag Handling""" def test_tag_variations(self): """Testing various ways tags need to be parsed""" # Test 1 - Key @@ -464,8 +473,8 @@ def test_tag_variations(self): {'context': 'CONTEXTLESS', 'key': '[][KeywithSquares]'}, ] - for i in range(0, len(test_tag_list)): - processed_tag = test_tag_list[i] + for i, test_tag_input in enumerate(test_tag_list): + processed_tag = test_tag_input self.assertTrue( (result := maintenance.parse_tag(processed_tag) ) == test_tag_expected_results[i], From c660f4abf30fe31e1af30892eff10be95bc168ce Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 00:16:54 -0400 Subject: [PATCH 041/204] PAF-46 #Pylint fixes for these files --- tests/test_host_groups.py | 12 ++++++++++-- tests/test_maintenance_windows.py | 6 +----- tests/test_topology_hosts.py | 2 +- tests/test_topology_process_groups.py | 2 +- tests/test_topology_services.py | 2 +- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/test_host_groups.py b/tests/test_host_groups.py index 2ce311a..ef78831 100644 --- a/tests/test_host_groups.py +++ b/tests/test_host_groups.py @@ -1,6 +1,6 @@ """Testing dynatrace.tenant.host_groups""" import unittest -import user_variables +import user_variables # pylint disable=import-error from tests import tooling_for_test from dynatrace.tenant import host_groups @@ -10,16 +10,24 @@ class TestHostGroupFunctions(unittest.TestCase): + """General Tests for Host Group Functions""" RESPONSE_DIR = "tests/mockserver_payloads/responses/host_groups/" def test_get_host_groups_tenantwide(self): + """Testing Retreival of all Host Groups within a single tenant""" parameters = { "relativeTime": ["day"], "includeDetails": ["true"], } mockserver_response_file = f"{self.RESPONSE_DIR}mock_get_general_1.json" tooling_for_test.create_mockserver_expectation( - CLUSTER, TENANT, URL_PATH, "GET", parameters=parameters, response_file=mockserver_response_file) + CLUSTER, + TENANT, + URL_PATH, + "GET", + parameters=parameters, + response_file=mockserver_response_file + ) command_tested = host_groups.get_host_groups_tenantwide( CLUSTER, TENANT) diff --git a/tests/test_maintenance_windows.py b/tests/test_maintenance_windows.py index 75942a6..b1d3a3f 100644 --- a/tests/test_maintenance_windows.py +++ b/tests/test_maintenance_windows.py @@ -1,6 +1,6 @@ """Test Cases For Maintenance Windows.""" import unittest -import user_variables +import user_variables # pylint: disable=import-error from tests import tooling_for_test from dynatrace.tenant import maintenance from dynatrace.requests.request_handler import TenantAPIs @@ -178,7 +178,6 @@ def test_create_once_no_scope(self): ) maintenance_schedule = maintenance.generate_schedule( maintenance.RecurrenceType.ONCE, - # TODO Remove need for these variables. ONCE does not use them "23:00", 60, TEST_RANGE_START, @@ -210,7 +209,6 @@ def test_create_weekly_no_scope(self): ) maintenance_schedule = maintenance.generate_schedule( maintenance.RecurrenceType.WEEKLY, - # TODO Remove need for these variables. ONCE does not use them "23:00", 60, TEST_RANGE_START, @@ -243,7 +241,6 @@ def test_create_monthly_no_scope(self): ) maintenance_schedule = maintenance.generate_schedule( maintenance.RecurrenceType.MONTHLY, - # TODO Remove need for these variables. ONCE does not use them "23:00", 60, TEST_RANGE_START, @@ -344,7 +341,6 @@ def test_no_day_of_month_supplied(self): def test_invalid_datetime_format(self): """Test invalid datetime supplied to trigger ValueError""" - # TODO Fix Exceoption to have a message as first arg with self.assertRaises(InvalidDateFormatException) as context: maintenance.generate_schedule( maintenance.RecurrenceType.DAILY, diff --git a/tests/test_topology_hosts.py b/tests/test_topology_hosts.py index ac99dc7..cdf4cd7 100644 --- a/tests/test_topology_hosts.py +++ b/tests/test_topology_hosts.py @@ -2,7 +2,7 @@ Test Suite for Topology Hosts """ import unittest -from user_variables import FULL_SET +from user_variables import FULL_SET # pylint disable=import-error from tests import tooling_for_test as testtools from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import hosts diff --git a/tests/test_topology_process_groups.py b/tests/test_topology_process_groups.py index 5f103b2..70a81b8 100644 --- a/tests/test_topology_process_groups.py +++ b/tests/test_topology_process_groups.py @@ -1,7 +1,7 @@ """Test Suite for Topology Process Groups""" import unittest -from user_variables import FULL_SET +from user_variables import FULL_SET # pylint disable=import-error from tests import tooling_for_test as testtools from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import process_groups diff --git a/tests/test_topology_services.py b/tests/test_topology_services.py index 68e02ac..5f58dae 100644 --- a/tests/test_topology_services.py +++ b/tests/test_topology_services.py @@ -1,7 +1,7 @@ """Test Suite for Topology Services""" import unittest -from user_variables import FULL_SET +from user_variables import FULL_SET # pylint disable=import-error from tests import tooling_for_test as testtools from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import services From dd917634c61b48458dd34f0b1f326dd8a266cabf Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 00:25:11 -0400 Subject: [PATCH 042/204] PAF-46 #Fixing disable linting comment --- tests/test_host_groups.py | 2 +- tests/test_topology_hosts.py | 2 +- tests/test_topology_process_groups.py | 2 +- tests/test_topology_services.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_host_groups.py b/tests/test_host_groups.py index ef78831..9d3c4ae 100644 --- a/tests/test_host_groups.py +++ b/tests/test_host_groups.py @@ -1,6 +1,6 @@ """Testing dynatrace.tenant.host_groups""" import unittest -import user_variables # pylint disable=import-error +import user_variables # pylint: disable=import-error from tests import tooling_for_test from dynatrace.tenant import host_groups diff --git a/tests/test_topology_hosts.py b/tests/test_topology_hosts.py index cdf4cd7..dca64bf 100644 --- a/tests/test_topology_hosts.py +++ b/tests/test_topology_hosts.py @@ -2,7 +2,7 @@ Test Suite for Topology Hosts """ import unittest -from user_variables import FULL_SET # pylint disable=import-error +from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import hosts diff --git a/tests/test_topology_process_groups.py b/tests/test_topology_process_groups.py index 70a81b8..0d6c099 100644 --- a/tests/test_topology_process_groups.py +++ b/tests/test_topology_process_groups.py @@ -1,7 +1,7 @@ """Test Suite for Topology Process Groups""" import unittest -from user_variables import FULL_SET # pylint disable=import-error +from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import process_groups diff --git a/tests/test_topology_services.py b/tests/test_topology_services.py index 5f58dae..115f2ee 100644 --- a/tests/test_topology_services.py +++ b/tests/test_topology_services.py @@ -1,7 +1,7 @@ """Test Suite for Topology Services""" import unittest -from user_variables import FULL_SET # pylint disable=import-error +from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import services From 544521909e9509cf1a0feadf667a596978acd98f Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 00:28:46 -0400 Subject: [PATCH 043/204] Trying to add another step to get report from linter --- .github/workflows/linter.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 86b0c8d..2147cff 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -40,6 +40,7 @@ jobs: - name: Checkout Code uses: actions/checkout@v2 + - run: mkdir -p super-linter.report ################################ # Run Linter against code base # ################################ @@ -52,6 +53,11 @@ jobs: PYTHON_PYLINT_CONFIG_FILE: .pylintrc GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LOG_FILE: super-linter.log + OUTPUT_FOLDER: super-linter.report + OUTPUT_FORMAT: tap + ############################ + # Retreive log file report # + ############################ - name: Archive super-linter log uses: actions/upload-artifact@v2 with: From 8c57fd894d71029d7e1ab896ac4abf25bb098133 Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 00:36:59 -0400 Subject: [PATCH 044/204] PAF-46 #Pulling the Artifacts of the full report --- .github/workflows/linter.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 2147cff..ec94394 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -45,7 +45,7 @@ jobs: # Run Linter against code base # ################################ - name: Lint Code Base - uses: docker://github/super-linter:v3 + uses: github/super-linter:v3 env: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master @@ -54,7 +54,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LOG_FILE: super-linter.log OUTPUT_FOLDER: super-linter.report - OUTPUT_FORMAT: tap ############################ # Retreive log file report # ############################ @@ -63,3 +62,11 @@ jobs: with: name: linter-report path: super-linter.log + ######################## + # Retreive tap reports # + ######################## + - name: Archive super-linter tap reports + uses: actions/upload-artifact@v2 + with: + name: linter-report + path: super-linter.report From c07a6b0ec5df677d731638ceeca892444860d251 Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 00:40:11 -0400 Subject: [PATCH 045/204] PAF-46 #Trying to fix syntax [skip-ci] --- .github/workflows/linter.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index ec94394..cf9c346 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -45,7 +45,7 @@ jobs: # Run Linter against code base # ################################ - name: Lint Code Base - uses: github/super-linter:v3 + uses: docker://github/super-linter:v3 env: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master @@ -60,7 +60,7 @@ jobs: - name: Archive super-linter log uses: actions/upload-artifact@v2 with: - name: linter-report + name: linter-log path: super-linter.log ######################## # Retreive tap reports # From 8cb4bfa1388d8779b1cb91612002acfee18329c6 Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 00:45:03 -0400 Subject: [PATCH 046/204] PAF-46 #GA Adjustments, Pylint linting --- .github/workflows/linter.yml | 2 +- tests/test_topology_hosts.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index cf9c346..fe2db2a 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -45,7 +45,7 @@ jobs: # Run Linter against code base # ################################ - name: Lint Code Base - uses: docker://github/super-linter:v3 + uses: github/super-linter@v3.11.0 env: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master diff --git a/tests/test_topology_hosts.py b/tests/test_topology_hosts.py index dca64bf..c25af78 100644 --- a/tests/test_topology_hosts.py +++ b/tests/test_topology_hosts.py @@ -82,8 +82,6 @@ def test_get_host_units(self): result = hosts.get_host_units_tenantwide(CLUSTER, TENANT) self.assertEqual(result, 4) - hosts.set_host_properties - class TestHostTagging(unittest.TestCase): """Test cases for testing host-level tagging.""" From 0b8d94de1d67aeda94de3452cb2d94946bae92dc Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 00:50:18 -0400 Subject: [PATCH 047/204] PAF-46 #GA Adjustments --- .github/workflows/linter.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index fe2db2a..50a043a 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -54,19 +54,12 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LOG_FILE: super-linter.log OUTPUT_FOLDER: super-linter.report - ############################ - # Retreive log file report # - ############################ - - name: Archive super-linter log - uses: actions/upload-artifact@v2 - with: - name: linter-log - path: super-linter.log ######################## # Retreive tap reports # ######################## - name: Archive super-linter tap reports uses: actions/upload-artifact@v2 + if: failure() with: name: linter-report path: super-linter.report From efbb3028c21987d59ebb6b64a2d639941a96c95a Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 00:55:47 -0400 Subject: [PATCH 048/204] PAF-46 #Yet Another GA Change [ci skip] --- .github/workflows/linter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 50a043a..fb2f5c4 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -54,6 +54,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} LOG_FILE: super-linter.log OUTPUT_FOLDER: super-linter.report + OUTPUT_FORMAT: tap ######################## # Retreive tap reports # ######################## From db4015196345ec02e5cbea5b7bb27c61923fc597 Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 01:09:19 -0400 Subject: [PATCH 049/204] PAF-46 #Pylint linting --- change_variables.py | 6 +++--- dynatrace/exceptions.py | 6 +++--- dynatrace/requests/request_handler.py | 3 ++- dynatrace/tenant/metrics.py | 1 + dynatrace/tenant/topology/applications.py | 1 + dynatrace/tenant/topology/custom.py | 1 + dynatrace/tenant/topology/process_groups.py | 8 +++++++- 7 files changed, 18 insertions(+), 8 deletions(-) diff --git a/change_variables.py b/change_variables.py index aba3923..e921d13 100644 --- a/change_variables.py +++ b/change_variables.py @@ -4,14 +4,14 @@ import os -def replace_set(set_file): +def replace_set(new_set_file): """Replace Variable File""" # Options are Darwin, Linux, Java and Windows. Java not supported if "Windows" in system(): os.system("copy variable_sets\\" + - str(set_file) + ".py user_variables.py") + str(new_set_file) + ".py user_variables.py") else: - os.system("cp variable_sets/" + str(set_file) + + os.system("cp variable_sets/" + str(new_set_file) + ".py user_variables.py") diff --git a/dynatrace/exceptions.py b/dynatrace/exceptions.py index 2d09e18..1d43279 100644 --- a/dynatrace/exceptions.py +++ b/dynatrace/exceptions.py @@ -7,21 +7,21 @@ class InvalidAPIResponseException (Exception): """The framework did not get an expected result from the Dynatrace API""" def __init__(self, message): - super(InvalidAPIResponseException, self).__init__(message) + super(InvalidAPIResponseException, self).__init__() print(message, file=stderr) class InvalidDateFormatException(ValueError): """The Date provided does not match the format needed""" def __init__(self, required_format): - super(InvalidDateFormatException, self).__init__(required_format) + super(InvalidDateFormatException, self).__init__() self.message = f"Incorrect Date for following entry: {required_format}" class InvalidScopeException(ValueError): """The Scope is incomplete or misconfigured""" def __init__(self, required_format): - super(InvalidScopeException, self).__init__(required_format) + super(InvalidScopeException, self).__init__() self.required_format = required_format print("Invalid scope used. Tag required for management zone, matching rule: %s", required_format, file=stderr) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 5e8fe57..40a51b8 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -146,7 +146,8 @@ def check_response(response): print(f"Waiting {time_to_wait} sec until the limit resets.") time.sleep(float(time_to_wait)) return False - elif not 200 <= response.status_code <= 299: + + if not 200 <= response.status_code <= 299: raise InvalidAPIResponseException( f"Response Error:\n{response.url}\n{response.status_code}\n{response.text}") diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index ed78db9..2d65ed4 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -1,3 +1,4 @@ +"""Module for interacting with the Metrics API""" from dynatrace.requests import request_handler as rh ENDPOINT = rh.TenantAPIs.METRICS diff --git a/dynatrace/tenant/topology/applications.py b/dynatrace/tenant/topology/applications.py index 083ee34..f6f4985 100644 --- a/dynatrace/tenant/topology/applications.py +++ b/dynatrace/tenant/topology/applications.py @@ -57,6 +57,7 @@ def get_application_count_clusterwide(cluster): def get_application_count_setwide(full_set): + """Get total count of applications in cluster set""" full_set_app_count = 0 for cluster_items in full_set.values(): full_set_app_count = full_set_app_count \ diff --git a/dynatrace/tenant/topology/custom.py b/dynatrace/tenant/topology/custom.py index 658f62f..3868379 100644 --- a/dynatrace/tenant/topology/custom.py +++ b/dynatrace/tenant/topology/custom.py @@ -1,3 +1,4 @@ +"""Module for interacting with Custom Topology Actions""" import dynatrace.tenant.topology.shared as topology_shared diff --git a/dynatrace/tenant/topology/process_groups.py b/dynatrace/tenant/topology/process_groups.py index e45d912..3c169c5 100644 --- a/dynatrace/tenant/topology/process_groups.py +++ b/dynatrace/tenant/topology/process_groups.py @@ -14,7 +14,13 @@ def get_process_group(cluster, tenant, entity): def set_process_group_properties(cluster, tenant, entity, prop_json): """Update properties of process-group entity""" - return topology_shared.set_env_layer_properties(cluster, tenant, 'process-groups', entity, prop_json) + return topology_shared.set_env_layer_properties( + cluster, + tenant, + 'process-groups', + entity, + prop_json + ) def get_process_group_count_tenantwide(cluster, tenant, params=None): From 5c37100fe34aa4c63faf47cb74f619d25a10e069 Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 01:10:35 -0400 Subject: [PATCH 050/204] PAF-46 #Debug GA --- .github/workflows/linter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index fb2f5c4..f35b3d6 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -55,6 +55,7 @@ jobs: LOG_FILE: super-linter.log OUTPUT_FOLDER: super-linter.report OUTPUT_FORMAT: tap + - run: ls ######################## # Retreive tap reports # ######################## From f688879fa1b46e073763a210c9c9f1863dd8b409 Mon Sep 17 00:00:00 2001 From: Philipose Date: Thu, 24 Sep 2020 09:35:48 -0400 Subject: [PATCH 051/204] PAF-46 #Pylinting & removing TODOs from lint fail --- .pylintrc | 3 +-- dynatrace/cluster/config.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index a865541..d7f9509 100644 --- a/.pylintrc +++ b/.pylintrc @@ -334,8 +334,7 @@ signature-mutators= # List of note tags to take in consideration, separated by a comma. notes=FIXME, - XXX, - TODO + XXX # Regular expression of note tags to take in consideration. #notes-rgx= diff --git a/dynatrace/cluster/config.py b/dynatrace/cluster/config.py index 0d51613..f4040f5 100644 --- a/dynatrace/cluster/config.py +++ b/dynatrace/cluster/config.py @@ -1,19 +1,45 @@ +"""Cluster Config Operations for Dynatrace Managed""" import dynatrace.requests.request_handler as rh def get_node_info(cluster): + """Get Current Cluster Information + + Args: + cluster (cluster dict): Currently selected cluster + + Returns: + dict: cluster node info dictionary + """ response = rh.make_api_call(cluster=cluster, endpoint=rh.ClusterAPIs.CLUSTER) return response.json() def get_node_config(cluster): + """Get current cluster config for each node + + Args: + cluster (cluster dict): Currently selected cluster + + Returns: + dict: current cluster configuration properties + """ response = rh.make_api_call(cluster=cluster, endpoint=rh.ClusterAPIs.CONFIG) return response.json() def set_node_config(cluster, json): + """Set cluster config for each node + + Args: + cluster (cluster dict): Currently selected cluster + json (dict): Dict of all desired settings + + Returns: + int: status code of pass/failed + """ response = rh.make_api_call(cluster=cluster, endpoint=rh.ClusterAPIs.CONFIG, method=rh.HTTP.POST, From 35406c3e9c4e65ccaf45e237700e789274da5548 Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 26 Sep 2020 09:23:15 -0400 Subject: [PATCH 052/204] PAF-46 #Pytlinting and removing outdated md --- .coverage | Bin 0 -> 53248 bytes Documentation/GETTING_STARTED.md | 0 Documentation/HOWTO.md | 0 Documentation/MODULE_GLOSSARY.md | 330 -------------------------- dynatrace/cluster/user_groups.py | 18 ++ dynatrace/cluster/users.py | 8 +- dynatrace/exceptions.py | 8 +- dynatrace/requests/request_handler.py | 2 +- dynatrace/settings.py | 26 +- dynatrace/tenant/request_naming.py | 12 +- dynatrace/tenant/timeseries.py | 13 + 11 files changed, 56 insertions(+), 361 deletions(-) create mode 100644 .coverage delete mode 100644 Documentation/GETTING_STARTED.md delete mode 100644 Documentation/HOWTO.md delete mode 100644 Documentation/MODULE_GLOSSARY.md diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..b5befdd33510b8f7ae15a1df33ba895bc248ab4f GIT binary patch literal 53248 zcmeI4UyK_^9mjXQYp=a)-v%95lN=!<1UN^}m%CgWH9={UHhHKT(LW@JP=m8)&z(21 zcb(n!{gI%l(~5`}kl<9rBS@r3ydY2uNT>w$g;uHreF7dxBoO~Z-YT`ICE+*w=WHi; zQ6*$YJ-#E`^Jiz~H}jd_%y@Qo=W|byii z;vrk^b8qF;rJ4I!!Th|G&%7X+UoL#XEa!hv*k}APznuHF@uGG%*Q5(DAOHd&00JNo z66ije*NQuL$``*HSPM-acvg*j(Qo0Ar;Z;zd7Pa*`tXV4EDB?Lbw;LKW=EOluCO-u z*rMI!%y#N_%?fO1i3JUw#P0aKE@nImqd8ARoC(tlSJkdls(>$17H!XNS>75u$JeIA z2(h$)uLe;7Wy0+xN5o)z3%XVuAD1ujuuhg&qmMCA z8w%)E<Z_1xCJz-3L_39IS&c3`^>O>WzT0M#w(Qk+pg9xdqD7-r9mhW3;Tw%V&3Y-P!)iG0 zG!vB|4fv#yDIOe`CDKa1>Ys1YAFry_3Ea?MrN*z$(NF&Q4`sFDo;~uV^P!>$JkW6^{w?r^pZg(5xuC{WbR_vWkhNgH4bTWf1lZxz_2{g(U==Yq>|oM zZ4-s1GO&fFyD+G0p~m=}ItYygNwmd|Qxjdq7j>w)%iOb;_)L5?8W!%hB9ZFMisuRbn&c%*wS5vj(#Dv_9dW0C4#5X-q2@%rk0=G;A@4xtMD`>FW z%PxYc;H7k?_?d~V3NDIJofE&A=blQ1MZTnk^jkFOi?1?JWD(LU@?(6hylgdTG-+G5 zM_rc2;AjS5B4XX5$ku|}2}1o9-cUWo+|w&>ltIs*u%}p4VRY)=B*;IMcvz`n`C{<# zOKib)o7{4uDS;wB>XRtjXQAlZC!>dYdH8E0kEP{OSd?T=5u^J_>T5~2B1fxbIrVs& z60>iVD-HRgPYOBF*g*+diTa{XJ@b}4lT72QVxUU$pJ@{ck!#DUN4;FMMPH694Srj8 z(Ij-t3V2|*I1^FA!i4&?V)?A*ak@xT=u}C$Q|pUHs?LdDu}aR0KQi4tvR1tJUb)+e z`(QM@RDGJuTUJu+*qa*Ajax4E)Z}p*XX#H)zj_FEeK>?Sk|4&ks09}y3Q4T^FOEJX z7WlIHu0$Ug5C8!X009sH0T2KI5C8!X009sHf$c{?kyElJ*8i%xE}3tbubAH_2Mh>+ z00@8p2!H?xfB*=900@8p2!Oyy2;@~|x0yVZF}q7qCg#Oc3HxXF&z5HomiHfEvj-2& z-GAu52MQ@=ay;JCzkgG|ftN+Yg^F zD1AvhDgcP_b0Aol+a{a=##HFMY4<+16)ONGPv*YZ{4sZN9wd(d*nRu6M3d--sG;$8v&7;I`eSyR=pzAga&P^wUr;Z~BNyCTMCEL?dun*{`_}(iU+vn%leFpjUt3YT=Z7xy_Vs^e zS?#)`lbfvn)1&P_ed~X9B#k?`{#VYa-P6N^Jh1*x(fH8G4eegT9oGLi{|^%*2!H?x zfB*=900@8p2!H?xfB*<=YXXX_NCmO~FPra4^nn2Z5C8!X009sH0T2KI5C8!X009u# zh6EI)kPF}cH$N?ze=&b=zGnW~{E7Kp^NZ#Q^C9y91;c;<2!H?xfB*=900@8p2!H?x zfWZG50o_pL+&#wS^(P+QtLfz4pW!NR=-V^y_?2;Kedjk->DrBVe*V^B`8R*~>HcpNtN$oHE6MUNexw#N zlzcXmx$(o_UVrRo`~LHtm)|Z)JET|Ag*09GX!^5L-+TUvfBrj{QOP@|rV5G(Rx(GV zw;!i1eLY3qds5$e^>0_?U9y_XJ@6oZulVPiH!sZp>EqImzy8kCsq~RgG&14)|K>}Q z`7WLRf8G46d6n)1{LXyceAWCV1;c;<2!H?xfB*=900@8p2!H?xfB*>GEdt`!fNU6{ zrR$-UqZ1W#wgLjn=oF2% literal 0 HcmV?d00001 diff --git a/Documentation/GETTING_STARTED.md b/Documentation/GETTING_STARTED.md deleted file mode 100644 index e69de29..0000000 diff --git a/Documentation/HOWTO.md b/Documentation/HOWTO.md deleted file mode 100644 index e69de29..0000000 diff --git a/Documentation/MODULE_GLOSSARY.md b/Documentation/MODULE_GLOSSARY.md deleted file mode 100644 index f23447a..0000000 --- a/Documentation/MODULE_GLOSSARY.md +++ /dev/null @@ -1,330 +0,0 @@ -# Function Glossary -*Explain all functions in one file to help find the ones you may need* -
-
- -*\* Asterisk means optional argument* -## dynatrace.cluster -### cluster_config.py -- get_node_info(Cluster Dict: cluster) - - Return: JSON Dict - - Status: Ready for Use - - Description: Get cluster node information, such as node id, Hardware Info, JVM info, URIs -- get_node_config(Cluster Dict: cluster) - - Return: JSON Dict - - Status: Ready for Use - - Description: Get node configurations such as, WebUI enabled, Agent enabled, id, IP Addresses, datacenter -- set_node_config(Cluster Dict: cluster, Dict: json) - - Return: HTTP Status Code - - Status: **UNTESTED** - - Description: Set node configurations such as, WebUI enabled, Agent enabled, id, IP Addresses, datacenter - -### ssl.py -Notes: -Entity Type can only be "COLLECTOR" or "SERVER" (case sensitive). -In addition, when pushing SSL certs via the API, it is HIGHLY RECOMMENDED to allow local logins (aka Non-SSO logins) during the change. Enable SSO-only after you can manually check the cluster to verify there are no issues with SSO. - -- get_cert_details (Cluster Dict: cluster, String: entity_type, String: entity_id) - - Return: Dict - - Status: Ready for Use - - Description: Get JSON of information about the current SSL certificate in use by a specific Server Node(For sure) or Cluster ActiveGate(I think?) -- get_cert_install_status(Cluster Dict: cluster, String: entity_id) - - Return: String - - Status: **UNTESTED** - - Description: Not sure fully of the usage. I think it is for getting the status of a certificate update. -- set_cert(Cluster Dict: cluster, String: entity_type, String: entity_id, Dict: ssl_json) - - Return: Dict - - Status: Ready to Use - -### sso.py -Notes: Some of these API commands are not advertised in the Cluster Management API - -- disable_sso (Cluster Dict: cluster) - - Return: HTTP Status Code - - State: Ready for Use - - Description: Turns off SSO in the environment. Can be especially useful if SSO breaks and you need to login with a local account -- enable_sso (Cluster Dict: cluster, Boolean: disable_local*, Boolean: groups_enabled*, Boolean: is_openid*) - - Return: HTTP Status Code - - State: Ready for Use (Only tested with already linked SSO) - - Description: Enables SSO that is already configured but disabled. By default, local login is still enabled, groups are not passed via SSO and uses SAML over OpenID. -- get_sso_status (Cluster Dict: cluster) - - Return: Dict - - State: Ready for Use - - Description: Shows the current authentication settings related to SSO - -### user_groups.py -- create_app_groups (String: app_name)
- - Return: Nothing - - Status: **LIMITED** - - Description: Takes the application and creates user groups for an application set-wide.
This is currently only applying a single format:
({User_Prefix}\_{Role_Type}\_{Tenant}_{User_Suffix})
User Prefix/Suffix and Role Type are set in the variable sets - - Current Plans: - - Refactor to a function that is for a single cluster and one for the set - - Ignore any SaaS environments in the set - - Allow for user group definited to be templated, so that a user can plug in their own group format - - Add Suffix logic -- delete_app_groups (String: app_name)
- - Return: Nothing - - Status: **LIMITED** - - Description: Takes the application and removes user groups for an application set-wide.
This is currently only applying a single format:
({User_Prefix}\_{Role_Type}\_{Tenant}_{User_Suffix})
User Prefix/Suffix and Role Type are set in the variable sets - - Current Plans: - - Refactor to a function that is for a single cluster and one for the set - - Ignore any SaaS environments in the set - - Allow for user group definited to be templated, so that a user can plug in their own group format - - Add Suffix Logic -- create_app_clusterwide (Cluster Dict: cluster, String: app_name, Dict of String List: zones*) - - Return: Nothing - - Status: **INCOMPLETE** - - Description: Create all user groups, and management zones and assign the new user groups to have appropriate permissions of the new management zones created
- "zones" is an optional argument. it is a dict of string lists. The intention is that each key would be the same as the cluster tenant name, and the embedded list will contain all the customer environments/lifecycles that will need their own management zone.
Management would be created in the format "{APP}" or "{APP} - {ENV}" - - Current Plans: - - Assign appropriate permissions to the user group from the new management zones - - Creating user groups has same limitations as "[create_app_groups](#create_app_groups)" - -### users.py -Module Notes: If SaaS is passed, by default it is ignored without error or notice. For notice, pass ignore_saas=False into the functions and it will raise an exception - -- check_is_managed(Cluster Dict: cluster, Boolean: ignore_saas) - - Return: If current cluster is Managed - - Status: Ready for Use - - Description: Internal function mostly to check if the cluster is Managed. -- get_users(Cluster Dict: cluster, Boolean: ignore_saas*) - - Return: JSON of users data in cluster - - Status: Ready for Use - - Description: Get all users in cluster and details. -- add_user(Cluster Dict: cluster, Dict: user_json, Boolean: ignore_saas*) - - Return: 'OK' - - Status: Ready for Use - - Description: Add user to the cluster according to user_json Dict -- update_user(Cluster Dict: cluster, Dict: user_json, Boolean: ignore_saas*) - - Return: 'OK' - - Status: Ready for Use - - Description: Update user information for the cluster according to user_json Dict -- get_user (Cluster Dict: cluster, String: user_id, Boolean: ignore_saas*) - - Return: JSON - - Status: Ready for Use - - Description: Get information for a single user by giving the user id -- delete_user (Cluster Dict: cluster, String: user_id, Boolean: ignore_saas*) - - Return: JSON - - Status: Ready for Use - - Description: Delete single user from the Managed Cluster -- add_user_bulk (Cluster Dict: cluster, Dict: user_json, Boolean: ignore_saas*) - - Return: 'OK' - - Status: Ready for Use - - Description: Add multiple users to the cluster according to the user_json Dict - -## dynatrace.requests - -### request_hander.py -*Class Notes:
-Cluster Dict is a single cluster defined in the FULL_SET set in user_variables and follows that structure
-Endpoints should not start with a "/"
-Params are Dict of parameters that are directly passed to the API, Key should match Dynatrace param name* - -- check_response (Cluster Dict: cluster, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: Checks if the response is within the HTTP 200-299 for a successful transaction. Otherwise raises an exception with the error
-- check_managed (Cluster Dict: cluster, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: Checks if the cluster instance provided is Managed or SaaS.
- - Current Plans: - - Allow ignore by default, so exception isn't raised and the function just carries on, skipping SaaS instances. -- cluster_get (Cluster Dict: cluster, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: GET Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- cluster_post (Cluster Dict: cluster, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: POST Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- cluster_put (Cluster Dict: cluster, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: PUT Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- cluster_delete (Cluster Dict: cluster, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: DELETE Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- env_get (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: GET Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- env_post (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: POST Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- env_put (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: PUT Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- env_delete (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: DELETE Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- config_get (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: GET Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- config_post (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: POST Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- config_put (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: PUT Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- config_delete (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Current Plans: - - Description: DELETE Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function - -## dynatrace.tenant - -### host_groups.py - -- get_host_groups_tenantwide(Cluster Dict: cluster, String: tenant) - - Return: Dict - - Status: Ready for Use - - Description: Get all Host Groups in a tenant. Dict uses HostGroup ID for the Key -- get_host_groups_tenantwide(Cluster Dict: cluster, String: tenant) -- get_host_groups_clusterwide (Cluster Dict: cluster) - - Return: Dict - - Status: Ready for Use - - Description: Get all Host Groups in a Cluster. Dict uses HostGroup ID for the Key - - Current Plan: - - Add split_by_tenant optional variable to return all host groups in nested Dicts by tenant -- get_host_groups_setwide (Dict of Cluster Dict: setwide) - - Return: Dict - - Status: Ready for Use - - Description: Get all Host Groups in the full_set of Clusters. Dict uses HostGroup ID for the Key - - Current Plan: - - Add split_by_tenant optional variable to return all host groups in nested Dicts by cluster and then again by tenant - -## dynatrace.timeseries - -### timeseries.py -Note: Currently V1 only - -- get_timeseries_list (Cluster Dict: cluster, String: tenant, Dict: params\*) - - Return: Dict - - Status: Ready for Use - - Description: Get list of Timeseries Metric Available -- get_timeseries_list (Cluster Dict: cluster, String: tenant, String metric, Dict: params\*) - - Return: List/Dict (Varies based on Metric) - - Status: Ready for Use - - Description: Get individual timeseries metric -- create_custom_metric (Cluster Dict: cluster, String: tenant, String metric, Dict: json, Dict: params\*) - - Return: HTTP Status Code - - Status: **Untested** - - Description: Create custom metric -- delete_custom_metric (Cluster Dict: cluster, String: tenant, String metric) - - Return: HTTP Status Code - - Status: **Untested** - - Description: Delete custom metric using metric ID - -## dynatrace.topology - -### applications.py - -- get_applications_tenantwide (Cluster Dict: cluster, String: Tenant) - - Return: Dict - - Status: Ready for Use - - Description: Returns JSON payload for the list of applications -- get_application (Cluster Dict: cluster, String: tenant, String: entity) - - Return: Dict - - Status: Ready for Use - - Description: Returns a specific application JSON payload referred by its Entity ID -- set_application_properties(Cluster Dict: cluster, String: tenant, String: entity, Dict: prop_json) - - Return: Dict - - Status: Ready for Use - - Description: Update Properties of the Application (at the moment the API only allows adding manual tags) -- get_application_count_tenantwide (Cluster Dict: cluster, String: Tenant) - - Return: Int - - Status: Ready for Use - - Description: Get the number of Applications defined in the tenant -- get_application_count_clusterwide (Cluster Dict: cluster) - - Return: Int - - Status: Ready for Use - - Description: Get the number of Applications defined in the cluster -- get_application_count_setwide (Dict of Cluster Dict: setwide) - - Return: Int - - Status: Ready for Use - - Description: Get the number of Applications defined all the clusters/instances in the set -- get_application_baseline(cluster, tenant, entity) - - Return: Dict - - Status: **UNTESTED** - - Description: Returns baseline information about the application requested - -### custom.py - -- set_custom_properties (Cluster Dict: cluster. String tenant, String: Entity, Dict: prop_json) - - Return: Dict - - Status: **UNTESTED** - - Description: Create/Update custom device. - -### hosts.py -- get_hosts_tenantwide (Cluster Dict: cluster, String: Tenant, Dict: params\*) - - Return: Dict - - Status: Ready for Use - - Description: Returns JSON payload for the list of hosts -- get_hosts_tenantwide (Cluster Dict: cluster, String: Tenant, String: Entity, Dict: params\*) - - Return: Dict - - Status: Ready for Use - - Description: Returns JSON payload for a single host -- set_host_properties(Cluster Dict: cluster, String: tenant, String: entity, Dict: prop_json) - - Return: Dict - - Status: Ready for Use - - Description: Update Properties of the host (at the moment the API only allows adding manual tags) -- get_host_count_clusterwide (Cluster Dict: cluster) - - Return: Int - - Status: Ready for Use - - Description: Get the number of hosts defined in the cluster -- get_host_count_setwide (Dict of Cluster Dict: setwide) - - Return: Int - - Status: Ready for Use - - Description: Get the number of hosts defined all the clusters/instances in the set -- add_host_tags (Cluster Dict: cluster, String: tenant, String: entity, List: tag_list) - - Return: HTTP Status Code - - Status: Ready for Use - - Description: Add tags to host -- get_host_units_tenantwide(Cluster Dict: cluster, String: tenant, List: params\*): - - Return: Number - - Status: Ready for Use - - Description: Tally host units consumed by tenant (can be filtered down with params) - -### process_groups.py -TODO - refer to above topology explanations for now -### process.py -TODO - refer to above topology explanations for now -### services.py -TODO - refer to above topology explanations for now - -### shared.py -NOTE: This is unifying shared operations of multiple layers of the topology. It is advised that you do not use this module and use the other topology functions built on top of this. - - diff --git a/dynatrace/cluster/user_groups.py b/dynatrace/cluster/user_groups.py index c073bac..74e1b19 100644 --- a/dynatrace/cluster/user_groups.py +++ b/dynatrace/cluster/user_groups.py @@ -13,6 +13,18 @@ def generate_group_name(template, user_type, tenant, app_name): + """Generate User Group according to template + + Args: + template (str): template with replacable values for variables + user_type (str): user permission type + tenant (str): tenant for user_group to match to + app_name (str): Application name + + Returns: + [type]: [description] + """ + #TODO Refactor for more replacements template = template.replace("{USER_TYPE}", user_type) template = template.replace("{TENANT}", tenant) template = template.replace("{APP_NAME}", app_name) @@ -59,6 +71,12 @@ def create_app_groups_setwide(app_name): def delete_app_groups(cluster, app_name): + """Delete Uesr Groups for Application + + Args: + cluster (cluster dict): Currently selected cluster + app_name (str): Application to remove all groups + """ role_types = user_variables.USER_GROUPS['role_types'] role_tenants = user_variables.USER_GROUPS['role_tenants'] diff --git a/dynatrace/cluster/users.py b/dynatrace/cluster/users.py index 2331503..bb68bde 100644 --- a/dynatrace/cluster/users.py +++ b/dynatrace/cluster/users.py @@ -51,9 +51,11 @@ def get_user(cluster, user_id, ignore_saas=True): def delete_user(cluster, user_id, ignore_saas=True): """Delete a Single User""" check_is_managed(cluster, ignore_saas) - response = rh.cluster_delete(cluster=cluster, - method=rh.HTTP.DELETE, - endpoint=f"{rh.ClusterAPIs.USERS}/{user_id}") + response = rh.make_api_call ( + cluster=cluster, + endpoint=f"{rh.ClusterAPIs.USERS}/{user_id}", + method=rh.HTTP.DELETE, + ) return response.json() diff --git a/dynatrace/exceptions.py b/dynatrace/exceptions.py index 1d43279..2b3d3b7 100644 --- a/dynatrace/exceptions.py +++ b/dynatrace/exceptions.py @@ -7,21 +7,21 @@ class InvalidAPIResponseException (Exception): """The framework did not get an expected result from the Dynatrace API""" def __init__(self, message): - super(InvalidAPIResponseException, self).__init__() + super().__init__(message) print(message, file=stderr) class InvalidDateFormatException(ValueError): """The Date provided does not match the format needed""" def __init__(self, required_format): - super(InvalidDateFormatException, self).__init__() + super().__init__() self.message = f"Incorrect Date for following entry: {required_format}" class InvalidScopeException(ValueError): """The Scope is incomplete or misconfigured""" def __init__(self, required_format): - super(InvalidScopeException, self).__init__() + super().__init__() self.required_format = required_format print("Invalid scope used. Tag required for management zone, matching rule: %s", required_format, file=stderr) @@ -30,5 +30,5 @@ def __init__(self, required_format): class ManagedClusterOnlyException(TypeError): """The operation is only supported on a managed cluster""" def __init__(self): - super(ManagedClusterOnlyException, TypeError).__init__() + super().__init__() print("This operation is only supported on Dynatrace Managed!", file=stderr) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 40a51b8..0c3f8d2 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -82,7 +82,7 @@ def __repr__(self): return str(self.name) -def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method=HTTP.GET): +def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method=HTTP.GET): # pylint: disable=too-many-arguments ''' Function makes an API call in a safe way, taking into account the rate limits. This will ensure the API call will always go through.\n diff --git a/dynatrace/settings.py b/dynatrace/settings.py index 8fdd032..c6a9bc1 100644 --- a/dynatrace/settings.py +++ b/dynatrace/settings.py @@ -6,10 +6,9 @@ FILE_IMPORTED = False -class DefaultSettings(): - """Default settings if not specified elsewhere""" - LOG_LEVEL = None - LOG_DIR = "logs/" +DefaultSettings = { + 'LOG_LEVEL': None, + 'LOG_DIR': "logs/", # ROLE TYPE KEYS # access_env @@ -19,7 +18,7 @@ class DefaultSettings(): # view_senstive # change_sensitive - USER_GROUPS = { + 'USER_GROUPS': { "role_types": { "access_env": "accessenv", "change_settings": "changesettings", @@ -30,10 +29,10 @@ class DefaultSettings(): "nonprod", "prod" ] - } - USER_GROUP_TEMPLATE = "prefix_{USER_TYPE}_{TENANT}_{APP_NAME}_suffix" - DEFAULT_TIMEZONE = "UTC" - + }, + 'USER_GROUP_TEMPLATE': "prefix_{USER_TYPE}_{TENANT}_{APP_NAME}_suffix", + 'DEFAULT_TIMEZONE': "UTC", +} def get_setting(attribute): """Fetch setting from user-defined files or else default values @@ -49,8 +48,7 @@ def get_setting(attribute): """ if FILE_IMPORTED and hasattr(user_variables, attribute): return getattr(user_variables, attribute) - elif hasattr(DefaultSettings, attribute): - return getattr(DefaultSettings, attribute) - else: - raise AttributeError( - f"{attribute} is not a valid user variable attribute!") + if attribute in DefaultSettings: + return DefaultSettings[attribute] + raise AttributeError( + f"{attribute} is not a valid user variable attribute!") diff --git a/dynatrace/tenant/request_naming.py b/dynatrace/tenant/request_naming.py index e284524..38cb48f 100644 --- a/dynatrace/tenant/request_naming.py +++ b/dynatrace/tenant/request_naming.py @@ -38,15 +38,9 @@ def pull_to_files(cluster, tenant, ignore_disabled=True): return rules_file_list -def push_from_files(file_list, cluster, tenant): - """Push Service Naming Rules from Files""" - # TODO add safeties - for file_name in file_list: - print(file_name) - - def generate_file_list(): + """Generate File List from files in JSON directory""" file_list = os.listdir("./jsons/request_naming/") - for f in file_list: - print(str.isdigit(f)) + for file_name in file_list: + print(str.isdigit(file_name)) # print(file_list.sort(key=lambda f: filter(str.isdigit, f))) diff --git a/dynatrace/tenant/timeseries.py b/dynatrace/tenant/timeseries.py index be9e8ce..23692b4 100644 --- a/dynatrace/tenant/timeseries.py +++ b/dynatrace/tenant/timeseries.py @@ -1,3 +1,4 @@ +"""Timerseries Operations from Environment V1 API""" from dynatrace.requests import request_handler as rh ENDPOINT = "timeseries/" @@ -17,6 +18,18 @@ def get_timeseries_metric(cluster, tenant, metric, params=None): def create_custom_metric(cluster, tenant, metric, json, params=None): + """Create a custome timeseries metric + + Args: + cluster (cluster dict): Currently selected cluster + tenant (str): tenant to pull metrics from + metric (str): selected metric to store as + json (dict): json payload required to created custom metric + params (dict, optional): [description]. Defaults to None. + + Returns: + int: response status code + """ response = rh.make_api_call(cluster=cluster, tenant=tenant, endpoint=f"{ENDPOINT}{metric}", From d2ea8e9a13c3bae14d21cc5637a2c4e6ddeb5bc9 Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 26 Sep 2020 09:25:12 -0400 Subject: [PATCH 053/204] Accidentally included debug file --- .coverage | Bin 53248 -> 0 bytes .gitignore | 1 + 2 files changed, 1 insertion(+) delete mode 100644 .coverage diff --git a/.coverage b/.coverage deleted file mode 100644 index b5befdd33510b8f7ae15a1df33ba895bc248ab4f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53248 zcmeI4UyK_^9mjXQYp=a)-v%95lN=!<1UN^}m%CgWH9={UHhHKT(LW@JP=m8)&z(21 zcb(n!{gI%l(~5`}kl<9rBS@r3ydY2uNT>w$g;uHreF7dxBoO~Z-YT`ICE+*w=WHi; zQ6*$YJ-#E`^Jiz~H}jd_%y@Qo=W|byii z;vrk^b8qF;rJ4I!!Th|G&%7X+UoL#XEa!hv*k}APznuHF@uGG%*Q5(DAOHd&00JNo z66ije*NQuL$``*HSPM-acvg*j(Qo0Ar;Z;zd7Pa*`tXV4EDB?Lbw;LKW=EOluCO-u z*rMI!%y#N_%?fO1i3JUw#P0aKE@nImqd8ARoC(tlSJkdls(>$17H!XNS>75u$JeIA z2(h$)uLe;7Wy0+xN5o)z3%XVuAD1ujuuhg&qmMCA z8w%)E<Z_1xCJz-3L_39IS&c3`^>O>WzT0M#w(Qk+pg9xdqD7-r9mhW3;Tw%V&3Y-P!)iG0 zG!vB|4fv#yDIOe`CDKa1>Ys1YAFry_3Ea?MrN*z$(NF&Q4`sFDo;~uV^P!>$JkW6^{w?r^pZg(5xuC{WbR_vWkhNgH4bTWf1lZxz_2{g(U==Yq>|oM zZ4-s1GO&fFyD+G0p~m=}ItYygNwmd|Qxjdq7j>w)%iOb;_)L5?8W!%hB9ZFMisuRbn&c%*wS5vj(#Dv_9dW0C4#5X-q2@%rk0=G;A@4xtMD`>FW z%PxYc;H7k?_?d~V3NDIJofE&A=blQ1MZTnk^jkFOi?1?JWD(LU@?(6hylgdTG-+G5 zM_rc2;AjS5B4XX5$ku|}2}1o9-cUWo+|w&>ltIs*u%}p4VRY)=B*;IMcvz`n`C{<# zOKib)o7{4uDS;wB>XRtjXQAlZC!>dYdH8E0kEP{OSd?T=5u^J_>T5~2B1fxbIrVs& z60>iVD-HRgPYOBF*g*+diTa{XJ@b}4lT72QVxUU$pJ@{ck!#DUN4;FMMPH694Srj8 z(Ij-t3V2|*I1^FA!i4&?V)?A*ak@xT=u}C$Q|pUHs?LdDu}aR0KQi4tvR1tJUb)+e z`(QM@RDGJuTUJu+*qa*Ajax4E)Z}p*XX#H)zj_FEeK>?Sk|4&ks09}y3Q4T^FOEJX z7WlIHu0$Ug5C8!X009sH0T2KI5C8!X009sHf$c{?kyElJ*8i%xE}3tbubAH_2Mh>+ z00@8p2!H?xfB*=900@8p2!Oyy2;@~|x0yVZF}q7qCg#Oc3HxXF&z5HomiHfEvj-2& z-GAu52MQ@=ay;JCzkgG|ftN+Yg^F zD1AvhDgcP_b0Aol+a{a=##HFMY4<+16)ONGPv*YZ{4sZN9wd(d*nRu6M3d--sG;$8v&7;I`eSyR=pzAga&P^wUr;Z~BNyCTMCEL?dun*{`_}(iU+vn%leFpjUt3YT=Z7xy_Vs^e zS?#)`lbfvn)1&P_ed~X9B#k?`{#VYa-P6N^Jh1*x(fH8G4eegT9oGLi{|^%*2!H?x zfB*=900@8p2!H?xfB*<=YXXX_NCmO~FPra4^nn2Z5C8!X009sH0T2KI5C8!X009u# zh6EI)kPF}cH$N?ze=&b=zGnW~{E7Kp^NZ#Q^C9y91;c;<2!H?xfB*=900@8p2!H?x zfWZG50o_pL+&#wS^(P+QtLfz4pW!NR=-V^y_?2;Kedjk->DrBVe*V^B`8R*~>HcpNtN$oHE6MUNexw#N zlzcXmx$(o_UVrRo`~LHtm)|Z)JET|Ag*09GX!^5L-+TUvfBrj{QOP@|rV5G(Rx(GV zw;!i1eLY3qds5$e^>0_?U9y_XJ@6oZulVPiH!sZp>EqImzy8kCsq~RgG&14)|K>}Q z`7WLRf8G46d6n)1{LXyceAWCV1;c;<2!H?xfB*=900@8p2!H?xfB*>GEdt`!fNU6{ zrR$-UqZ1W#wgLjn=oF2% diff --git a/.gitignore b/.gitignore index 852b2a8..1c60234 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ templates/* # Framework log files ***.log** ***.tap** +.converge user_variables.py sandbox_script.py From b873a937519260db453f86885e7baaf37cf5d11b Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 26 Sep 2020 09:43:05 -0400 Subject: [PATCH 054/204] PAF-46 #Flake8 Linting --- dynatrace/cluster/user_groups.py | 2 +- dynatrace/cluster/users.py | 2 +- dynatrace/requests/request_handler.py | 2 +- dynatrace/settings.py | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dynatrace/cluster/user_groups.py b/dynatrace/cluster/user_groups.py index 74e1b19..7c4b7e8 100644 --- a/dynatrace/cluster/user_groups.py +++ b/dynatrace/cluster/user_groups.py @@ -24,7 +24,7 @@ def generate_group_name(template, user_type, tenant, app_name): Returns: [type]: [description] """ - #TODO Refactor for more replacements + # TODO Refactor for more replacements template = template.replace("{USER_TYPE}", user_type) template = template.replace("{TENANT}", tenant) template = template.replace("{APP_NAME}", app_name) diff --git a/dynatrace/cluster/users.py b/dynatrace/cluster/users.py index bb68bde..ff1f8a3 100644 --- a/dynatrace/cluster/users.py +++ b/dynatrace/cluster/users.py @@ -51,7 +51,7 @@ def get_user(cluster, user_id, ignore_saas=True): def delete_user(cluster, user_id, ignore_saas=True): """Delete a Single User""" check_is_managed(cluster, ignore_saas) - response = rh.make_api_call ( + response = rh.make_api_call( cluster=cluster, endpoint=f"{rh.ClusterAPIs.USERS}/{user_id}", method=rh.HTTP.DELETE, diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 0c3f8d2..40a51b8 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -82,7 +82,7 @@ def __repr__(self): return str(self.name) -def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method=HTTP.GET): # pylint: disable=too-many-arguments +def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method=HTTP.GET): ''' Function makes an API call in a safe way, taking into account the rate limits. This will ensure the API call will always go through.\n diff --git a/dynatrace/settings.py b/dynatrace/settings.py index c6a9bc1..3f9cdfd 100644 --- a/dynatrace/settings.py +++ b/dynatrace/settings.py @@ -34,6 +34,7 @@ 'DEFAULT_TIMEZONE': "UTC", } + def get_setting(attribute): """Fetch setting from user-defined files or else default values From 574fb47130fe33ebdd3370feb5c32d6a2735639a Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 26 Sep 2020 10:05:28 -0400 Subject: [PATCH 055/204] PAF-46 #Pylinting --- dynatrace/cluster/user_groups.py | 2 +- dynatrace/settings.py | 2 +- dynatrace/tenant/management_zones.py | 7 +++---- dynatrace/tenant/timeseries.py | 10 ++++++++++ dynatrace/tenant/topology/hosts.py | 20 ++++++++++++++++---- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/dynatrace/cluster/user_groups.py b/dynatrace/cluster/user_groups.py index 7c4b7e8..170467e 100644 --- a/dynatrace/cluster/user_groups.py +++ b/dynatrace/cluster/user_groups.py @@ -1,6 +1,6 @@ #!/bin/python3 """Cluster Group Operations""" -import user_variables +import user_variables # pylint: disable=import-error from dynatrace.requests import request_handler as rh from dynatrace.tenant import management_zones as mzh diff --git a/dynatrace/settings.py b/dynatrace/settings.py index 3f9cdfd..deb3be0 100644 --- a/dynatrace/settings.py +++ b/dynatrace/settings.py @@ -1,6 +1,6 @@ """Module for retreiving settings safely""" try: - import user_variables + import user_variables # pylint: disable=import-error FILE_IMPORTED = True except ImportError: FILE_IMPORTED = False diff --git a/dynatrace/tenant/management_zones.py b/dynatrace/tenant/management_zones.py index 192c587..f336940 100644 --- a/dynatrace/tenant/management_zones.py +++ b/dynatrace/tenant/management_zones.py @@ -27,8 +27,8 @@ def generate_mz_payload(application, env_zone=None): application) for rule_num in range(2, 10): - mz_payload['rules'][rule_num]['conditions'][0]['comparisonInfo']['value']['key'] = "APP: " + \ - str(application) + mz_payload['rules'][rule_num]['conditions'][0]['comparisonInfo']['value']['key'] = \ + "APP: " + str(application) if env_zone: # If environment exists, rename MZ and add environment conditions @@ -72,8 +72,7 @@ def add_management_zone(cluster, tenant, application, env_zone=None): json=mz_payload) if "id" in response.json(): return (response.json())['id'] - else: - return (response.text) + return response.text def change_management_zone(cluster, tenant, mz_id, application, env_zone=None): diff --git a/dynatrace/tenant/timeseries.py b/dynatrace/tenant/timeseries.py index 23692b4..89873b0 100644 --- a/dynatrace/tenant/timeseries.py +++ b/dynatrace/tenant/timeseries.py @@ -40,6 +40,16 @@ def create_custom_metric(cluster, tenant, metric, json, params=None): def delete_custom_metic(cluster, tenant, metric): + """[summary] + + Args: + cluster (cluster dict): Currently selected cluster + tenant (str): Tenant to operate in + metric (str): custom metric to be deleted + + Returns: + [type]: [description] + """ response = rh.make_api_call(cluster=cluster, tenant=tenant, method=rh.HTTP.DELETE, diff --git a/dynatrace/tenant/topology/hosts.py b/dynatrace/tenant/topology/hosts.py index 822725c..772d821 100644 --- a/dynatrace/tenant/topology/hosts.py +++ b/dynatrace/tenant/topology/hosts.py @@ -42,13 +42,25 @@ def delete_host_tag(cluster, tenant, entity, tag): """Remove single tag from host""" if tag is None: raise TypeError("Tag cannot be None!") - return rh.make_api_call(cluster=cluster, - tenant=tenant, - method=rh.HTTP.DELETE, - endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts/{entity}/tags/{tag}") + return rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.DELETE, + endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts/{entity}/tags/{tag}" + ) def get_host_units_tenantwide(cluster, tenant, params=None): + """Get Host Units used by tenant + + Args: + cluster (cluster dict): Currently selected cluster + tenant (str): Tenant to operate in + params (dict, optional): Available parameters to filter by. Defaults to None. + + Returns: + float: total consumed units used in tenant + """ consumed_host_units = 0 host_list = get_hosts_tenantwide(cluster, tenant, params=params) for host in host_list: From bb90832737bba208b4144635e18e12e21eb2caac Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 26 Sep 2020 12:01:01 -0400 Subject: [PATCH 056/204] PAF-46 #Flake8 Linting --- dynatrace/tenant/topology/hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/tenant/topology/hosts.py b/dynatrace/tenant/topology/hosts.py index 772d821..1419fb3 100644 --- a/dynatrace/tenant/topology/hosts.py +++ b/dynatrace/tenant/topology/hosts.py @@ -59,7 +59,7 @@ def get_host_units_tenantwide(cluster, tenant, params=None): params (dict, optional): Available parameters to filter by. Defaults to None. Returns: - float: total consumed units used in tenant + float: total consumed units used in tenant """ consumed_host_units = 0 host_list = get_hosts_tenantwide(cluster, tenant, params=params) From 74928fa990b2d6022df909739c4279980c2c003c Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 26 Sep 2020 12:24:54 -0400 Subject: [PATCH 057/204] PAF-46 #Pylinting [skip ci] --- dynatrace/tenant/topology/shared.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dynatrace/tenant/topology/shared.py b/dynatrace/tenant/topology/shared.py index 0148877..47f9c39 100644 --- a/dynatrace/tenant/topology/shared.py +++ b/dynatrace/tenant/topology/shared.py @@ -127,6 +127,21 @@ def get_set_layer_count(full_set, layer, params=None): def add_env_layer_tags(cluster, tenant, layer, entity, tag_list): + """Add tags to entity Layer + + Args: + cluster (cluster dict): Currently selected cluster + tenant (str): Tenant to operate in + layer (str): Topology Layer to pull from + entity (str): Entity to add tags to + tag_list (list): All tags to add to entity + + Raises: + TypeError: [description] + + Returns: + [type]: [description] + """ layer_list = ['applications', 'hosts', 'custom', 'process-groups', 'services'] check_valid_layer(layer, layer_list) From 3d82384980ae8161e9bec425014164360c03cac5 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 06:09:50 +0100 Subject: [PATCH 058/204] Changed slow-down to only apply on SaaS --- dynatrace/requests/request_handler.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index f6b42bb..09a449e 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -92,13 +92,20 @@ def slow_down(func): def wrapper(*args, **kwargs): response = func(*args, **kwargs) - # Standard Dynatrace response headers - req_remaining = int(response.headers.get('x-ratelimit-remaining')) - req_limit = int(response.headers.get('x-ratelimit-limit')) - # If 75% requests already made, slow down - print(f"{req_remaining} = {(req_remaining/req_limit)*100}%") - if req_remaining/req_limit <= 0.25: - time.sleep(1) + # Get the cluster from wrapped function + if 'cluster' in kwargs: + cluster = kwargs.get('cluster') + else: + cluster = args[0] + + # Only slow-down SaaS + if not cluster.get('is_managed'): + # Standard Dynatrace response headers + req_remaining = int(response.headers.get('x-ratelimit-remaining')) + req_limit = int(response.headers.get('x-ratelimit-limit')) + # If 75% requests already made, slow down + if req_remaining/req_limit <= 0.25: + time.sleep(1) return response return wrapper From 0696ff460dfc39bf127a9dc967e93787c144f7aa Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 09:36:14 +0100 Subject: [PATCH 059/204] shared operations remodeled for V2 API --- dynatrace/tenant/topology/shared.py | 358 ++++++++++++++++++++-------- 1 file changed, 257 insertions(+), 101 deletions(-) diff --git a/dynatrace/tenant/topology/shared.py b/dynatrace/tenant/topology/shared.py index 47f9c39..0add4fb 100644 --- a/dynatrace/tenant/topology/shared.py +++ b/dynatrace/tenant/topology/shared.py @@ -1,153 +1,309 @@ -"""Shared topology operations for multiple layers from the Dynatrace API""" +from enum import Enum, auto from dynatrace.requests import request_handler as rh -# Layer Compatibility -# 1. Get all entities - application, host, process, process group, service -# 1a. Count all entities -# 2. Get specific entity - application, host process, process group, service -# 3. Update properties of entity - application, custom, host, process group, service - -ENDPOINT_SUFFIX = { - 'applications': 'applications', - 'custom': "infrastructure/custom", - 'hosts': "infrastructure/hosts", - 'processes': "infrastructure/processes", - 'process-groups': "infrastructure/process-groups", - 'services': "infrastructure/services" -} - - -def check_valid_layer(layer, layer_list): - """Check if the operation is valid for the layer""" - if layer is None or layer_list is None: - raise TypeError('Provide layer and layer_list!') - if layer not in layer_list: - raise ValueError( - layer + " layer does not exist or is invalid for this use!") - - -def get_env_layer_entities(cluster, tenant, layer, params=None): - """Get all Entities of Specified Layer""" - layer_list = ['applications', 'hosts', - 'processes', 'process-groups', 'services'] - check_valid_layer(layer, layer_list) + +class EntityTypes(Enum): + """Accepted values for EntityType arguments""" + HTTP_CHECK = auto() + RELATIONAL_DATABASE_SERVICE = auto() + APPLICATION = auto() + KUBERNETES_NODE = auto() + CONTAINER_GROUP_INSTANCE = auto() + OPENSTACK_COMPUTE_NODE = auto() + QUEUE = auto() + EBS_VOLUME = auto() + OPENSTACK_PROJECT = auto() + PROCESS_GROUP = auto() + EC2_INSTANCE = auto() + GEOLOC_SITE = auto() + DEVICE_APPLICATION_METHOD_GROUP = auto() + AWS_AVAILABILITY_ZONE = auto() + SYNTHETIC_TEST_STEP = auto() + AZURE_STORAGE_ACCOUNT = auto() + AZURE_IOT_HUB = auto() + AWS_APPLICATION_LOAD_BALANCER = auto() + CLOUD_APPLICATION_NAMESPACE = auto() + BROWSER = auto() + GEOLOCATION = auto() + HTTP_CHECK_STEP = auto() + HYPERVISOR_DISK = auto() + AZURE_APP_SERVICE_PLAN = auto() + NEUTRON_SUBNET = auto() + S3BUCKET = auto() + NETWORK_INTERFACE = auto() + QUEUE_INSTANCE = auto() + APPLICATION_METHOD_GROUP = auto() + GCP_ZONE = auto() + OPENSTACK_VM = auto() + MOBILE_APPLICATION = auto() + PROCESS_GROUP_INSTANCE = auto() + HOST_GROUP = auto() + SYNTHETIC_LOCATION = auto() + SERVICE_INSTANCE = auto() + GOOGLE_COMPUTE_ENGINE = auto() + AZURE_SERVICE_BUS_TOPIC = auto() + AZURE_TENANT = auto() + CLOUD_APPLICATION = auto() + AZURE_EVENT_HUB = auto() + DEVICE_APPLICATION_METHOD = auto() + AZURE_SERVICE_BUS_NAMESPACE = auto() + VIRTUALMACHINE = auto() + ELASTIC_LOAD_BALANCER = auto() + AZURE_SUBSCRIPTION = auto() + AZURE_REDIS_CACHE = auto() + AWS_NETWORK_LOAD_BALANCER = auto() + BOSH_DEPLOYMENT = auto() + EXTERNAL_SYNTHETIC_TEST_STEP = auto() + DOCKER_CONTAINER_GROUP_INSTANCE = auto() + APPLICATION_METHOD = auto() + AZURE_CREDENTIALS = auto() + AZURE_MGMT_GROUP = auto() + SERVICE_METHOD_GROUP = auto() + AZURE_FUNCTION_APP = auto() + AZURE_SQL_SERVER = auto() + AZURE_SQL_DATABASE = auto() + AZURE_VM = auto() + OPENSTACK_AVAILABILITY_ZONE = auto() + SWIFT_CONTAINER = auto() + CLOUD_APPLICATION_INSTANCE = auto() + SERVICE = auto() + VMWARE_DATACENTER = auto() + AZURE_EVENT_HUB_NAMESPACE = auto() + VCENTER = auto() + AZURE_SERVICE_BUS_QUEUE = auto() + SERVICE_METHOD = auto() + OS = auto() + CONTAINER_GROUP = auto() + AWS_CREDENTIALS = auto() + AZURE_SQL_ELASTIC_POOL = auto() + DATASTORE = auto() + HYPERVISOR_CLUSTER = auto() + SYNTHETIC_TEST = auto() + EXTERNAL_SYNTHETIC_TEST = auto() + AUTO_SCALING_GROUP = auto() + CUSTOM_APPLICATION = auto() + AZURE_API_MANAGEMENT_SERVICE = auto() + DISK = auto() + HYPERVISOR = auto() + CUSTOM_DEVICE = auto() + AZURE_REGION = auto() + CINDER_VOLUME = auto() + DOCKER_CONTAINER_GROUP = auto() + KUBERNETES_CLUSTER = auto() + AZURE_WEB_APP = auto() + HOST = auto() + AZURE_LOAD_BALANCER = auto() + OPENSTACK_REGION = auto() + AWS_LAMBDA_FUNCTION = auto() + AZURE_APPLICATION_GATEWAY = auto() + AZURE_VM_SCALE_SET = auto() + AZURE_COSMOS_DB = auto() + DYNAMO_DB_TABLE = auto() + CUSTOM_DEVICE_GROUP = auto() + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + +def get_entities(cluster, tenant, entity_type, params=None): + """Get all Entities of specified type. Use EntityTypes enum.""" if not params: params = {} + # If params already contains entitySelector, don't overwrite + if params.get('entitySelector'): + params['entitySelector'] += f'type("{entity_type}")' + else: + params['entitySelector'] = f'type("{entity_type}")' + response = rh.make_api_call( cluster=cluster, tenant=tenant, - endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/{ENDPOINT_SUFFIX[layer]}", + endpoint=rh.TenantAPIs.ENTITIES, params=params ) return response.json() -def get_env_layer_entity(cluster, tenant, layer, entity, params=None): - """Get Entity Information for Specified Layer""" - layer_list = ['applications', 'hosts', - 'processes', 'process-groups', 'services'] - check_valid_layer(layer, layer_list) - +def get_entity(cluster, tenant, entity_id, params=None): + """ + Get the details of an entity specified by ID. + You can use more than one ID if it's quoted and comma separated ("id-1","id-2") + """ if not params: params = {} + # If params already contains entitySelector, don't overwrite + if params.get('entitySelector'): + params['entitySelector'] += f'entityId("{entity_id}")' + else: + params['entitySelector'] = f'entityId("{entity_id}")' + response = rh.make_api_call( cluster=cluster, tenant=tenant, - endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/{ENDPOINT_SUFFIX[layer]}/{entity}", + endpoint=rh.TenantAPIs.ENTITIES, params=params ) return response.json() -def set_env_layer_properties(cluster, tenant, layer, entity, prop_json): - """Update Properties of Entity""" - layer_list = ['applications', 'custom', - 'hosts', 'process-groups', 'services'] - check_valid_layer(layer, layer_list) - response = rh.make_api_call( - cluster=cluster, - tenant=tenant, - method=rh.HTTP.POST, - endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/{ENDPOINT_SUFFIX[layer]}/{entity}", - json=prop_json - ) - return response.status_code - - -def get_env_layer_count(cluster, tenant, layer, params=None): - """Get total hosts in an environment""" +def get_env_entity_count(cluster, tenant, entity_type, params=None): + """ + Get total number of entities of a given type in an environment. + Use EntityType enum. + """ if not params: params = {} - layer_list = ['applications', 'hosts', - 'processes', 'process-groups', 'services'] + if 'from' not in params: + params['from'] = "now-24h" - if 'relativeTime' not in params.keys(): - params['relativeTime'] = "day" - if 'includeDetails' not in params.keys(): - params['includeDetails'] = False + # If params already contains entitySelector, don't overwrite + if params.get('entitySelector'): + params['entitySelector'] += f'type("{entity_type}")' + else: + params['entitySelector'] = f'type("{entity_type}")' - check_valid_layer(layer, layer_list) response = rh.make_api_call(cluster=cluster, tenant=tenant, - endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/{ENDPOINT_SUFFIX[layer]}", + endpoint=rh.TenantAPIs.ENTITIES, params=params) env_layer_count = len(response.json()) return env_layer_count -def get_cluster_layer_count(cluster, layer, params=None): - """Get total count for all environments in cluster""" - +def get_cluster_entity_count(cluster, entity_type, params=None): + """ + Get total number of entitites of a given type for all environments + in cluster. Use EntityType enum. + """ if not params: params = {} - cluster_layer_count = 0 - for env_key in cluster['tenant']: - cluster_layer_count += get_env_layer_count(cluster=cluster, - tenant=env_key, - layer=layer, - params=params) - return cluster_layer_count + count = 0 + for tenant in cluster['tenant']: + count += get_env_entity_count( + cluster=cluster, + tenant=tenant, + entity_type=entity_type, + params=params + ) + return count -def get_set_layer_count(full_set, layer, params=None): +def get_set_entity_count(full_set, entity_type, params=None): """Get total count for all clusters definied in variable file""" if not params: params = {} - full_set_layer_count = 0 + count = 0 for cluster in full_set.values(): - full_set_layer_count += get_cluster_layer_count(cluster, - layer, - params) - return full_set_layer_count + count += get_cluster_entity_count( + cluster=cluster, + entity_type=entity_type, + params=params + ) + return count -def add_env_layer_tags(cluster, tenant, layer, entity, tag_list): - """Add tags to entity Layer +def add_tags(cluster, tenant, tag_list, entity_type=None, entity_id=None, params=None): + """ + Add tags to entities. Must specify tag key. Must specify at least + an Entity Type or ID.\n - Args: - cluster (cluster dict): Currently selected cluster - tenant (str): Tenant to operate in - layer (str): Topology Layer to pull from - entity (str): Entity to add tags to - tag_list (list): All tags to add to entity + @param cluster - Dynatrace Cluster\n + @param tenant - Dynatrace Tenant\n + @param tag_list - list of tags as dictionaries with "key" and + optionally "value" attributes\n + @param entity_type - use EntityTypes enum for this\n + @param entity_id - ID of entity. You can specify several IDs, quoted and + separated by a comma ("id-1","id-2").\n + @param params - other query string parameters compatible with this API.\n\n + @throws TypeError - if tag_list is empty or not a list\n + @throws ValueError - if neither entity_type nor entity_id are specified + """ + # Sanity checking, error handling + if not tag_list: + raise TypeError("No tags provided") + elif not isinstance(tag_list, list): + raise TypeError("tags_list is not a list") + elif not any([entity_type, entity_id]): + raise ValueError("Must specifiy at least either entity_type or entity_id") + + # Params may already contain an entitySelector, we mustn't overwrite + if entity_type: + if params['entitySelector']: + params['entitySelector'] += f'type("{entity_type}")' + else: + params['entitySelector'] = f'type("{entity_type}")' + if entity_id: + if params['entitySelector']: + params['entitySelector'] += f'entityId({entity_id})' + else: + params['entitySelector'] = f'type({entity_id})' + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.POST, + endpoint=rh.TenantAPIs.TAGS, + params=params + ) + + return response - Raises: - TypeError: [description] - Returns: - [type]: [description] +def delete_tag(cluster, tenant, tag_key, entity_type=None, entity_id=None, + tag_value=None, params=None): """ - layer_list = ['applications', 'hosts', - 'custom', 'process-groups', 'services'] - check_valid_layer(layer, layer_list) - if not tag_list: - raise TypeError("tag_list cannot be None type") - tag_json = { - 'tags': tag_list - } - return set_env_layer_properties(cluster, tenant, layer, entity, tag_json) + Delete tag from entities. Must specify at least an Entity Type or ID.\n + + @param cluster - Dynatrace Cluster\n + @param tenant - Dynatrace Tenant\n + @param tag_key - the key of the tag(s) to be deleted\n + @param tag_value - the values for the tag key to be deleted. + Use "all" to delete all values for the key.\n + @param entity_type - use EntityTypes enum for this\n + @param entity_id - ID of entity. You can specify several IDs, quoted and + separated by a comma ("id-1","id-2").\n + @param params - other query string parameters compatible with this API.\n\n + @throws TypeError - if tag_key is empty or missing\n + @throws ValueError - if neither entity_type nor entity_id are specified + """ + # Sanity checking, error handling + if not tag_key: + raise TypeError("No tag key provided") + elif not any([entity_type, entity_id]): + raise ValueError("Must specifiy at least either entity_type or entity_id") + + # Params may already contain an entitySelector, we mustn't overwrite + if entity_type: + if params.get('entitySelector'): + params['entitySelector'] += f'type("{entity_type}")' + else: + params['entitySelector'] = f'type("{entity_type}")' + if entity_id: + if params.get('entitySelector'): + params['entitySelector'] += f'entityId({entity_id})' + else: + params['entitySelector'] = f'type({entity_id})' + + # Set params for tag key & value + params['key'] = tag_key + if tag_value == "all": + params['deleteAllWithKey'] = True + elif tag_value: + params['value'] = tag_value + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.DELETE, + endpoint=rh.TenantAPIs.TAGS, + params=params + ) + return response From 72310bf3e8eb7613ddf286e677dae8133fa291af Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 09:48:10 +0100 Subject: [PATCH 060/204] simplified v2_get_results_by_page --- dynatrace/requests/request_handler.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 16e5d6a..da64d60 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -245,31 +245,25 @@ def v2_get_results_by_page(cluster, endpoint, item, tenant=None, params=None): Gets a multi-paged result set one page at a time. To be used with V2 API pagination where the nextPageKey is returned in the body of the response. \n - @param item - item being retrieved (e.g. entities, metrics, etc.)\n @param cluster - Cluster dictionary from variable_set\n @param endpoint - API endpoint to call.\n @param tenant - String of tenant name used in cluster dictionary\n @param params - dictionary of query string parameters """ - # Make the first API call; retrieve summary info and first page of results - response = make_api_call( - cluster=cluster, - endpoint=endpoint, - tenant=tenant, - params=params - ).json() - cursor = response.get('nextPageKey') - # Pause here and return 1st page - yield response - - # On subsequent calls, yield page by page the remaining result set + # We'll always make at least 1 call + cursor = 1 while cursor: + # On subsequent calls, must omit all other params + if cursor != 1: + params = dict(nextPageKey=cursor) + response = make_api_call( cluster=cluster, endpoint=endpoint, tenant=tenant, - params=dict(nextPageKey=cursor) + params=params ).json() + yield response cursor = response.get('nextPageKey') From 66cea1f9a76eb2fda5067929b655b116dee4edcc Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 09:50:04 +0100 Subject: [PATCH 061/204] removed changes already covered by PAF-35 branch --- dynatrace/requests/request_handler.py | 146 -------------------------- 1 file changed, 146 deletions(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 09a449e..794a134 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -154,152 +154,6 @@ def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method return response -def __get_v2_multipage_results(cluster, endpoint, cursor, item, tenant=None): - """ - Private function: not intended for calling from outside of this module. - Retrieves subsequent pages of multi-page API call and gathers just the - items requested through the endpoint (e.g. entities, metrics, etc.) - \n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param cursor - cursor that was returned with the first page of results\n - @param item - item being retrieved (e.g. entities, metrics, etc.) - """ - results_full = [] - while cursor: - results_page = make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=endpoint, - params=dict(nextPageKey=cursor) - ).json() - - # Collect just the items being queried - results_full.extend(results_page.get(item)) - - # Renew cursor - cursor = results_page.get('nextPageKey') - - return results_full - - -def v2_get_results_whole(cluster, endpoint, item, tenant=None, params=None): - """ - Gets a multi-paged result set and returns it whole. To be used with V2 API - pagination where the nextPageKey is returned in the body of the response. - Also this type of query requires the queried item so we can extract it from - the subsequent pages and omit the summary data. - \n - @param item - item being retrieved (e.g. entities, metrics, etc.)\n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param params - dictionary of query string parameters - """ - # Get the first results set (including cursor) - response = make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=endpoint, - params=params - ).json() - - # In the case of multi-page, get the rest - cursor = response.get('nextPageKey') - if cursor: - response[item].extend(__get_v2_multipage_results( - cluster=cluster, - endpoint=endpoint, - tenant=tenant, - cursor=cursor, - item=item - )) - - return response - - -def v1_get_results_whole(cluster, endpoint, tenant=None, params=None): - """ - Gets a multi-paged result set and returns it whole. To be used with V1 API - pagination where the next-page-key is returned in the response headers. - \n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param params - dictionary of query string parameters - """ - results = [] - # We'll always make at least 1 call - cursor = 1 - while cursor: - if cursor != 1: - params['nextPageKey'] = cursor - response = make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=endpoint, - params=params - ) - results.extend(response.json()) - cursor = response.headers.get('next-page-key') - - return results - - -def v1_get_results_by_page(cluster, endpoint, tenant=None, params=None): - """ - Gets a multi-paged result set one page at a time. To be used with V1 API - pagination where the next-page-key is returned in the response headers. - \n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param params - dictionary of query string parameters - """ - cursor = 1 - while cursor: - if cursor != 1: - params['nextPageKey'] = cursor - response = make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=endpoint, - params=params - ) - # Pause here and return this page of results - yield response.json() - cursor = response.headers.get('next-page-key') - - -def v2_get_results_by_page(cluster, endpoint, tenant=None, params=None): - """ - Gets a multi-paged result set one page at a time. To be used with V2 API - pagination where the nextPageKey is returned in the body of the response. - \n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param params - dictionary of query string parameters - """ - # We'll always make at least 1 call - cursor = 1 - while cursor: - # On subsequent calls, must omit all other params - if cursor != 1: - params = dict(nextPageKey=cursor) - - response = make_api_call( - cluster=cluster, - endpoint=endpoint, - tenant=tenant, - params=params - ).json() - - yield response - cursor = response.get('nextPageKey') - - def check_response(response): ''' Checks if the Response has a Successful Status Code From 4d76d52b48414d120374c66b6680181613bbf977 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 10:58:27 +0100 Subject: [PATCH 062/204] adapted function returns. fixed other issues --- dynatrace/tenant/topology/shared.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/dynatrace/tenant/topology/shared.py b/dynatrace/tenant/topology/shared.py index 0add4fb..4c9645b 100644 --- a/dynatrace/tenant/topology/shared.py +++ b/dynatrace/tenant/topology/shared.py @@ -125,7 +125,7 @@ def get_entities(cluster, tenant, entity_type, params=None): endpoint=rh.TenantAPIs.ENTITIES, params=params ) - return response.json() + return response.json().get('entities') def get_entity(cluster, tenant, entity_id, params=None): @@ -148,7 +148,11 @@ def get_entity(cluster, tenant, entity_id, params=None): endpoint=rh.TenantAPIs.ENTITIES, params=params ) - return response.json() + + if len(response.json().get('entities')) == 1: + return response.json().get('entities')[0] + else: + return response.json().get('entities') def get_env_entity_count(cluster, tenant, entity_type, params=None): @@ -172,7 +176,7 @@ def get_env_entity_count(cluster, tenant, entity_type, params=None): tenant=tenant, endpoint=rh.TenantAPIs.ENTITIES, params=params) - env_layer_count = len(response.json()) + env_layer_count = response.json().get('totalCount') return env_layer_count @@ -226,6 +230,9 @@ def add_tags(cluster, tenant, tag_list, entity_type=None, entity_id=None, params @throws TypeError - if tag_list is empty or not a list\n @throws ValueError - if neither entity_type nor entity_id are specified """ + if not params: + params = {} + # Sanity checking, error handling if not tag_list: raise TypeError("No tags provided") @@ -236,22 +243,23 @@ def add_tags(cluster, tenant, tag_list, entity_type=None, entity_id=None, params # Params may already contain an entitySelector, we mustn't overwrite if entity_type: - if params['entitySelector']: + if params.get('entitySelector'): params['entitySelector'] += f'type("{entity_type}")' else: params['entitySelector'] = f'type("{entity_type}")' if entity_id: - if params['entitySelector']: + if params.get('entitySelector'): params['entitySelector'] += f'entityId({entity_id})' else: - params['entitySelector'] = f'type({entity_id})' + params['entitySelector'] = f'entityId({entity_id})' response = rh.make_api_call( cluster=cluster, tenant=tenant, method=rh.HTTP.POST, endpoint=rh.TenantAPIs.TAGS, - params=params + params=params, + json=dict(tags=tag_list) ) return response @@ -265,7 +273,7 @@ def delete_tag(cluster, tenant, tag_key, entity_type=None, entity_id=None, @param cluster - Dynatrace Cluster\n @param tenant - Dynatrace Tenant\n @param tag_key - the key of the tag(s) to be deleted\n - @param tag_value - the values for the tag key to be deleted. + @param tag_value - the value for the tag key to be deleted. Use "all" to delete all values for the key.\n @param entity_type - use EntityTypes enum for this\n @param entity_id - ID of entity. You can specify several IDs, quoted and @@ -274,6 +282,9 @@ def delete_tag(cluster, tenant, tag_key, entity_type=None, entity_id=None, @throws TypeError - if tag_key is empty or missing\n @throws ValueError - if neither entity_type nor entity_id are specified """ + if not params: + params = {} + # Sanity checking, error handling if not tag_key: raise TypeError("No tag key provided") @@ -290,7 +301,7 @@ def delete_tag(cluster, tenant, tag_key, entity_type=None, entity_id=None, if params.get('entitySelector'): params['entitySelector'] += f'entityId({entity_id})' else: - params['entitySelector'] = f'type({entity_id})' + params['entitySelector'] = f'entityId({entity_id})' # Set params for tag key & value params['key'] = tag_key From 5c07c5bdb3b02e2f325ff9da6763ceffd766ec0c Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 12:01:06 +0100 Subject: [PATCH 063/204] updated topology.applications to make use of the new topology.shared --- dynatrace/tenant/topology/applications.py | 85 ++++++++++++----------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/dynatrace/tenant/topology/applications.py b/dynatrace/tenant/topology/applications.py index f6f4985..87f2c1f 100644 --- a/dynatrace/tenant/topology/applications.py +++ b/dynatrace/tenant/topology/applications.py @@ -1,68 +1,69 @@ -"""Application operations from the Dynatrace API""" -# Applications needs a seperate definition since the url is not the same (not /infrastructre/) -from dynatrace.requests import request_handler as rh - -ENDPOINT = f"{rh.TenantAPIs.V1_TOPOLOGY}/applications" +import dynatrace.tenant.topology.shared as entity +import dynatrace.requests.request_handler as rh def get_applications_tenantwide(cluster, tenant): """Get Information for all applications in a tenant""" - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=ENDPOINT) - return response.json() + return entity.get_entities( + cluster=cluster, + tenant=tenant, + entity_type=entity.EntityTypes.APPLICATION + ) def get_application(cluster, tenant, entity): - """Get Information on one application for in a tenant""" - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=f"{ENDPOINT}/{entity}") - return response.json() + """Get Information for one application in a tenant""" + return entity.get_entity( + cluster=cluster, + tenant=tenant, + entity_id=entity + ) def set_application_properties(cluster, tenant, entity, prop_json): """Update properties of application entity""" - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=f"{ENDPOINT}/{entity}", - method=rh.HTTP.POST, - json=prop_json) + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=rh.TenantAPIs.TAGS, + params={ + 'entitySelector': f'entityId("{entity}")' + }, + method=rh.HTTP.POST, + json=prop_json + ) + return response.json() def get_application_count_tenantwide(cluster, tenant): """Get total count for all applications in a tenant""" params = { - "relativeTime": "day", - "includeDetails": "false" + "from": "now-24h" } - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=ENDPOINT, - params=params) - env_app_count = len(response.json()) - return env_app_count + return entity.get_env_entity_count( + cluster=cluster, + tenant=tenant, + entity_type=entity.EntityTypes.APPLICATION, + params=params + ) def get_application_count_clusterwide(cluster): """Get total count for all applications in cluster""" - cluster_app_count = 0 - for env_key in cluster['tenant']: - cluster_app_count = cluster_app_count \ - + get_application_count_tenantwide(cluster, - env_key) - return cluster_app_count + return entity.get_cluster_entity_count( + cluster=cluster, + entity_type=entity.EntityTypes.APPLICATION + ) def get_application_count_setwide(full_set): """Get total count of applications in cluster set""" - full_set_app_count = 0 - for cluster_items in full_set.values(): - full_set_app_count = full_set_app_count \ - + get_application_count_clusterwide(cluster_items) - return full_set_app_count + return entity.get_set_entity_count( + full_set=full_set, + entity_type=entity.EntityTypes.APPLICATION + ) def add_application_tags(cluster, tenant, entity, tag_list): @@ -77,7 +78,9 @@ def add_application_tags(cluster, tenant, entity, tag_list): def get_application_baseline(cluster, tenant, entity): """Get baselines on one application for in a tenant""" - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=f"{ENDPOINT}/{entity}/baseline") + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/applications/{entity}/baseline" + ) return response.json() From 76be2b9472afeebdb8dd64c5d1b843ed945f1638 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 12:06:16 +0100 Subject: [PATCH 064/204] resolved naming conflict on imports --- dynatrace/tenant/topology/applications.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dynatrace/tenant/topology/applications.py b/dynatrace/tenant/topology/applications.py index 87f2c1f..3763174 100644 --- a/dynatrace/tenant/topology/applications.py +++ b/dynatrace/tenant/topology/applications.py @@ -1,19 +1,19 @@ -import dynatrace.tenant.topology.shared as entity +import dynatrace.tenant.topology.shared as entity_api import dynatrace.requests.request_handler as rh def get_applications_tenantwide(cluster, tenant): """Get Information for all applications in a tenant""" - return entity.get_entities( + return entity_api.get_entities( cluster=cluster, tenant=tenant, - entity_type=entity.EntityTypes.APPLICATION + entity_type=entity_api.EntityTypes.APPLICATION ) def get_application(cluster, tenant, entity): """Get Information for one application in a tenant""" - return entity.get_entity( + return entity_api.get_entity( cluster=cluster, tenant=tenant, entity_id=entity @@ -42,27 +42,27 @@ def get_application_count_tenantwide(cluster, tenant): "from": "now-24h" } - return entity.get_env_entity_count( + return entity_api.get_env_entity_count( cluster=cluster, tenant=tenant, - entity_type=entity.EntityTypes.APPLICATION, + entity_type=entity_api.EntityTypes.APPLICATION, params=params ) def get_application_count_clusterwide(cluster): """Get total count for all applications in cluster""" - return entity.get_cluster_entity_count( + return entity_api.get_cluster_entity_count( cluster=cluster, - entity_type=entity.EntityTypes.APPLICATION + entity_type=entity_api.EntityTypes.APPLICATION ) def get_application_count_setwide(full_set): """Get total count of applications in cluster set""" - return entity.get_set_entity_count( + return entity_api.get_set_entity_count( full_set=full_set, - entity_type=entity.EntityTypes.APPLICATION + entity_type=entity_api.EntityTypes.APPLICATION ) From 35651396b9f9d9b8466f0525cac5d1d55fc0b802 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 13:09:39 +0100 Subject: [PATCH 065/204] updated topology.hosts to work with new topology.shared functions --- dynatrace/tenant/topology/hosts.py | 76 ++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 19 deletions(-) diff --git a/dynatrace/tenant/topology/hosts.py b/dynatrace/tenant/topology/hosts.py index 1419fb3..50f55fe 100644 --- a/dynatrace/tenant/topology/hosts.py +++ b/dynatrace/tenant/topology/hosts.py @@ -1,52 +1,86 @@ -"""Host operations from the Dynatrace API""" -import dynatrace.tenant.topology.shared as topology_shared -from dynatrace.requests import request_handler as rh +import dynatrace.tenant.topology.shared as entity_api +import dynatrace.requests.request_handler as rh def get_hosts_tenantwide(cluster, tenant, params=None): """Get Information for all hosts in a tenant""" - return topology_shared.get_env_layer_entities(cluster, tenant, 'hosts', params=params) + return entity_api.get_entities( + cluster=cluster, + tenant=tenant, + entity_type=entity_api.EntityTypes.HOST + ) def get_host(cluster, tenant, entity, params=None): - """Get Information on one host for in a tenant""" - return topology_shared.get_env_layer_entity(cluster, tenant, 'hosts', entity, params=params) + """Get Information for one host in a tenant""" + return entity_api.get_entity( + cluster=cluster, + tenant=tenant, + entity_id=entity + ) def set_host_properties(cluster, tenant, entity, prop_json): """Update properties of host entity""" - return topology_shared.set_env_layer_properties(cluster, tenant, 'hosts', entity, prop_json) + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=rh.TenantAPIs.TAGS, + params={ + 'entitySelector': f'entityId("{entity}")' + }, + method=rh.HTTP.POST, + json=prop_json + ) + + return response.json() def get_host_count_tenantwide(cluster, tenant, params=None): """Get total count for all hosts in a tenant""" - return topology_shared.get_env_layer_count(cluster, tenant, 'hosts', params=params) + return entity_api.get_env_entity_count( + cluster=cluster, + tenant=tenant, + entity_type=entity_api.EntityTypes.HOST, + params=params + ) def get_host_count_clusterwide(cluster, params=None): """Get total count for all hosts in cluster""" - return topology_shared.get_cluster_layer_count(cluster, 'hosts', params=params) + return entity_api.get_cluster_entity_count( + cluster=cluster, + entity_type=entity_api.EntityTypes.HOST, + params=params + ) def get_host_count_setwide(full_set, params=None): - """Get total count of hosts for all clusters definied in variable file""" - return topology_shared.get_set_layer_count(full_set, 'hosts', params=params) + """Get total count of hosts in cluster set""" + return entity_api.get_set_entity_count( + full_set=full_set, + entity_type=entity_api.EntityTypes.HOST, + params=params + ) def add_host_tags(cluster, tenant, entity, tag_list): """Add tags to host""" - return topology_shared.add_env_layer_tags(cluster, tenant, 'hosts', entity, tag_list) + return entity_api.add_tags( + cluster=cluster, + tenant=tenant, + tag_list=tag_list, + entity_id=entity + ) def delete_host_tag(cluster, tenant, entity, tag): """Remove single tag from host""" - if tag is None: - raise TypeError("Tag cannot be None!") - return rh.make_api_call( + return entity_api.delete_tag( cluster=cluster, tenant=tenant, - method=rh.HTTP.DELETE, - endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts/{entity}/tags/{tag}" + tag_key=tag, + entity_id=entity ) @@ -62,9 +96,13 @@ def get_host_units_tenantwide(cluster, tenant, params=None): float: total consumed units used in tenant """ consumed_host_units = 0 - host_list = get_hosts_tenantwide(cluster, tenant, params=params) + host_list = rh.make_api_call( + cluster, + tenant, + endpoint=f'{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts', + params=params) for host in host_list: - consumed_host_units = consumed_host_units + host['consumedHostUnits'] + consumed_host_units += host['consumedHostUnits'] return consumed_host_units From 196c6d13672dc1d7e6844e6a56c2566edf263a5d Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 13:13:38 +0100 Subject: [PATCH 066/204] added unused params to calls --- dynatrace/tenant/topology/hosts.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dynatrace/tenant/topology/hosts.py b/dynatrace/tenant/topology/hosts.py index 50f55fe..89c13e6 100644 --- a/dynatrace/tenant/topology/hosts.py +++ b/dynatrace/tenant/topology/hosts.py @@ -7,7 +7,8 @@ def get_hosts_tenantwide(cluster, tenant, params=None): return entity_api.get_entities( cluster=cluster, tenant=tenant, - entity_type=entity_api.EntityTypes.HOST + entity_type=entity_api.EntityTypes.HOST, + params=params ) @@ -16,7 +17,8 @@ def get_host(cluster, tenant, entity, params=None): return entity_api.get_entity( cluster=cluster, tenant=tenant, - entity_id=entity + entity_id=entity, + params=params ) From 9530bd990617bdc0679cf03e7881a3637c762b60 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 13:20:50 +0100 Subject: [PATCH 067/204] updated topology.process_groups to use the new topology.shared functions --- dynatrace/tenant/topology/process_groups.py | 71 +++++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/dynatrace/tenant/topology/process_groups.py b/dynatrace/tenant/topology/process_groups.py index 3c169c5..84415f3 100644 --- a/dynatrace/tenant/topology/process_groups.py +++ b/dynatrace/tenant/topology/process_groups.py @@ -1,43 +1,74 @@ -"""Process Group operations from the Dynatrace API""" -import dynatrace.tenant.topology.shared as topology_shared +import dynatrace.tenant.topology.shared as entity_api +import dynatrace.requests.request_handler as rh def get_process_groups_tenantwide(cluster, tenant): - """Get Information for all process-groups in a tenant""" - return topology_shared.get_env_layer_entities(cluster, tenant, 'process-groups') + """Get Information for all process_groups in a tenant""" + return entity_api.get_entities( + cluster=cluster, + tenant=tenant, + entity_type=entity_api.EntityTypes.PROCESS_GROUP + ) def get_process_group(cluster, tenant, entity): - """Get Information on one process-group for in a tenant""" - return topology_shared.get_env_layer_entity(cluster, tenant, 'process-groups', entity) + """Get Information for one process_group in a tenant""" + return entity_api.get_entity( + cluster=cluster, + tenant=tenant, + entity_id=entity + ) def set_process_group_properties(cluster, tenant, entity, prop_json): - """Update properties of process-group entity""" - return topology_shared.set_env_layer_properties( - cluster, - tenant, - 'process-groups', - entity, - prop_json + """Update properties of process_group entity""" + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=rh.TenantAPIs.TAGS, + params={ + 'entitySelector': f'entityId("{entity}")' + }, + method=rh.HTTP.POST, + json=prop_json ) + return response.json() + def get_process_group_count_tenantwide(cluster, tenant, params=None): - """Get total count for all process-groups in a tenant""" - return topology_shared.get_env_layer_count(cluster, tenant, 'process-groups', params=params) + """Get total count for all process_groups in a tenant""" + return entity_api.get_env_entity_count( + cluster=cluster, + tenant=tenant, + entity_type=entity_api.EntityTypes.PROCESS_GROUP, + params=params + ) def get_process_group_count_clusterwide(cluster, params=None): - """Get total count for all process-groups in cluster""" - return topology_shared.get_cluster_layer_count(cluster, 'process-groups', params=params) + """Get total count for all process_groups in cluster""" + return entity_api.get_cluster_entity_count( + cluster=cluster, + entity_type=entity_api.EntityTypes.PROCESS_GROUP, + params=params + ) def get_process_group_count_setwide(full_set, params=None): - """Get total count of process-groups for all clusters defined in variable file""" - return topology_shared.get_set_layer_count(full_set, 'process-groups', params=params) + """Get total count of process_groups in cluster set""" + return entity_api.get_set_entity_count( + full_set=full_set, + entity_type=entity_api.EntityTypes.PROCESS_GROUP, + params=params + ) def add_process_group_tags(cluster, tenant, entity, tag_list): """Add tags to a process group""" - return topology_shared.add_env_layer_tags(cluster, tenant, 'process-groups', entity, tag_list) + return entity_api.add_tags( + cluster=cluster, + tenant=tenant, + tag_list=tag_list, + entity_id=entity + ) From da2b014aad7d325594a1cefb0ada0b8b9877070e Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 13:23:25 +0100 Subject: [PATCH 068/204] updated topology.processes to use the new topology.shared functions --- dynatrace/tenant/topology/process.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/dynatrace/tenant/topology/process.py b/dynatrace/tenant/topology/process.py index fb6c523..c07ba28 100644 --- a/dynatrace/tenant/topology/process.py +++ b/dynatrace/tenant/topology/process.py @@ -1,12 +1,21 @@ -"""Process operations from the Dynatrace API""" -import dynatrace.tenant.topology.shared as topology_shared +import dynatrace.tenant.topology.shared as entity_api def get_processes_tenantwide(cluster, tenant, params=None): """Get Information for all processes in a tenant""" - return topology_shared.get_env_layer_entities(cluster, tenant, 'processes', params=params) + return entity_api.get_entities( + cluster=cluster, + tenant=tenant, + entity_type=entity_api.EntityTypes.PROCESS_GROUP_INSTANCE, + params=params + ) def get_process(cluster, tenant, entity, params=None): """Get Information on one process for in a tenant""" - return topology_shared.get_env_layer_entity(cluster, tenant, 'processes', entity, params=params) + return entity_api.get_entity( + cluster=cluster, + tenant=tenant, + entity_id=entity, + params=params + ) From 130c3a9df85e09dea85da3efc8196f96987d1b7a Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 17:32:34 +0100 Subject: [PATCH 069/204] updated topology.services to use the new topology.shared functions --- dynatrace/tenant/topology/services.py | 59 ++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/dynatrace/tenant/topology/services.py b/dynatrace/tenant/topology/services.py index 6b4fe58..98e8a43 100644 --- a/dynatrace/tenant/topology/services.py +++ b/dynatrace/tenant/topology/services.py @@ -1,37 +1,74 @@ -"""Service operations from the Dynatrace API""" -import dynatrace.tenant.topology.shared as topology_shared +import dynatrace.tenant.topology.shared as entity_api +import dynatrace.requests.request_handler as rh def get_services_tenantwide(cluster, tenant): """Get Information for all services in a tenant""" - return topology_shared.get_env_layer_entities(cluster, tenant, 'services') + return entity_api.get_entities( + cluster=cluster, + tenant=tenant, + entity_type=entity_api.EntityTypes.SERVICE + ) def get_service(cluster, tenant, entity): - """Get Information on one service for in a tenant""" - return topology_shared.get_env_layer_entity(cluster, tenant, 'services', entity) + """Get Information for one service in a tenant""" + return entity_api.get_entity( + cluster=cluster, + tenant=tenant, + entity_id=entity + ) def set_service_properties(cluster, tenant, entity, prop_json): """Update properties of service entity""" - return topology_shared.set_env_layer_properties(cluster, tenant, 'services', entity, prop_json) + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=rh.TenantAPIs.TAGS, + params={ + 'entitySelector': f'entityId("{entity}")' + }, + method=rh.HTTP.POST, + json=prop_json + ) + + return response.json() def get_service_count_tenantwide(cluster, tenant, params=None): """Get total count for all services in a tenant""" - return topology_shared.get_env_layer_count(cluster, tenant, 'services', params=params) + return entity_api.get_env_entity_count( + cluster=cluster, + tenant=tenant, + entity_type=entity_api.EntityTypes.SERVICE, + params=params + ) def get_service_count_clusterwide(cluster, params=None): """Get total count for all services in cluster""" - return topology_shared.get_cluster_layer_count(cluster, 'services', params=params) + return entity_api.get_cluster_entity_count( + cluster=cluster, + entity_type=entity_api.EntityTypes.SERVICE, + params=params + ) def get_service_count_setwide(full_set, params=None): - """Get total count of services for all clusters definied in variable file""" - return topology_shared.get_set_layer_count(full_set, 'services', params=params) + """Get total count of services in cluster set""" + return entity_api.get_set_entity_count( + full_set=full_set, + entity_type=entity_api.EntityTypes.SERVICE, + params=params + ) def add_service_tags(cluster, tenant, entity, tag_list): """Add tags to a service""" - return topology_shared.add_env_layer_tags(cluster, tenant, 'services', entity, tag_list) + return entity_api.add_tags( + cluster=cluster, + tenant=tenant, + tag_list=tag_list, + entity_id=entity + ) \ No newline at end of file From d9c78438056cc483bf103a32d79312aa12bb9422 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 17:37:00 +0100 Subject: [PATCH 070/204] cleaned up comments --- dynatrace/tenant/topology/process_groups.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dynatrace/tenant/topology/process_groups.py b/dynatrace/tenant/topology/process_groups.py index 84415f3..34f242e 100644 --- a/dynatrace/tenant/topology/process_groups.py +++ b/dynatrace/tenant/topology/process_groups.py @@ -3,7 +3,7 @@ def get_process_groups_tenantwide(cluster, tenant): - """Get Information for all process_groups in a tenant""" + """Get Information for all process groups in a tenant""" return entity_api.get_entities( cluster=cluster, tenant=tenant, @@ -12,7 +12,7 @@ def get_process_groups_tenantwide(cluster, tenant): def get_process_group(cluster, tenant, entity): - """Get Information for one process_group in a tenant""" + """Get Information for one process group in a tenant""" return entity_api.get_entity( cluster=cluster, tenant=tenant, @@ -21,7 +21,7 @@ def get_process_group(cluster, tenant, entity): def set_process_group_properties(cluster, tenant, entity, prop_json): - """Update properties of process_group entity""" + """Update properties of process group entity""" response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -37,7 +37,7 @@ def set_process_group_properties(cluster, tenant, entity, prop_json): def get_process_group_count_tenantwide(cluster, tenant, params=None): - """Get total count for all process_groups in a tenant""" + """Get total count for all process groups in a tenant""" return entity_api.get_env_entity_count( cluster=cluster, tenant=tenant, @@ -47,7 +47,7 @@ def get_process_group_count_tenantwide(cluster, tenant, params=None): def get_process_group_count_clusterwide(cluster, params=None): - """Get total count for all process_groups in cluster""" + """Get total count for all process groups in cluster""" return entity_api.get_cluster_entity_count( cluster=cluster, entity_type=entity_api.EntityTypes.PROCESS_GROUP, @@ -56,7 +56,7 @@ def get_process_group_count_clusterwide(cluster, params=None): def get_process_group_count_setwide(full_set, params=None): - """Get total count of process_groups in cluster set""" + """Get total count of process groups in cluster set""" return entity_api.get_set_entity_count( full_set=full_set, entity_type=entity_api.EntityTypes.PROCESS_GROUP, From 4befd4144987fc965f5e62c3dab8c639ea6a009d Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 17:47:04 +0100 Subject: [PATCH 071/204] updated topology.custom to use the new topology.shared functions --- dynatrace/tenant/topology/custom.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dynatrace/tenant/topology/custom.py b/dynatrace/tenant/topology/custom.py index 3868379..f1a247d 100644 --- a/dynatrace/tenant/topology/custom.py +++ b/dynatrace/tenant/topology/custom.py @@ -1,7 +1,15 @@ -"""Module for interacting with Custom Topology Actions""" -import dynatrace.tenant.topology.shared as topology_shared +import dynatrace.requests.request_handler as rh def set_custom_properties(cluster, tenant, entity, prop_json): - """Update properties of process_group entity""" - return topology_shared.set_env_layer_properties(cluster, tenant, 'custom', entity, prop_json) + """Creates or updates properties of custom device entity""" + if not prop_json.get('customDeviceId'): + prop_json['customDeviceId'] = entity + + return rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f'{rh.TenantAPIs.ENTITIES}/custom', + method=rh.HTTP.POST, + json=prop_json + ) From 2f06b527a7fa17d8db0747f8aa6da8e456dedac3 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 8 Oct 2020 20:34:55 +0100 Subject: [PATCH 072/204] changes to pass linting checks --- dynatrace/tenant/topology/applications.py | 2 ++ dynatrace/tenant/topology/custom.py | 4 +++- dynatrace/tenant/topology/hosts.py | 2 ++ dynatrace/tenant/topology/process.py | 2 ++ dynatrace/tenant/topology/process_groups.py | 2 ++ dynatrace/tenant/topology/services.py | 4 +++- dynatrace/tenant/topology/shared.py | 12 +++++++----- 7 files changed, 21 insertions(+), 7 deletions(-) diff --git a/dynatrace/tenant/topology/applications.py b/dynatrace/tenant/topology/applications.py index 3763174..10b20ba 100644 --- a/dynatrace/tenant/topology/applications.py +++ b/dynatrace/tenant/topology/applications.py @@ -1,3 +1,5 @@ +"""Module for application type entity operations""" + import dynatrace.tenant.topology.shared as entity_api import dynatrace.requests.request_handler as rh diff --git a/dynatrace/tenant/topology/custom.py b/dynatrace/tenant/topology/custom.py index f1a247d..ca1edb8 100644 --- a/dynatrace/tenant/topology/custom.py +++ b/dynatrace/tenant/topology/custom.py @@ -1,3 +1,5 @@ +"""Module or custom device type entity operations""" + import dynatrace.requests.request_handler as rh @@ -5,7 +7,7 @@ def set_custom_properties(cluster, tenant, entity, prop_json): """Creates or updates properties of custom device entity""" if not prop_json.get('customDeviceId'): prop_json['customDeviceId'] = entity - + return rh.make_api_call( cluster=cluster, tenant=tenant, diff --git a/dynatrace/tenant/topology/hosts.py b/dynatrace/tenant/topology/hosts.py index 89c13e6..f187ca1 100644 --- a/dynatrace/tenant/topology/hosts.py +++ b/dynatrace/tenant/topology/hosts.py @@ -1,3 +1,5 @@ +"""Module for host type entity operations""" + import dynatrace.tenant.topology.shared as entity_api import dynatrace.requests.request_handler as rh diff --git a/dynatrace/tenant/topology/process.py b/dynatrace/tenant/topology/process.py index c07ba28..8b25119 100644 --- a/dynatrace/tenant/topology/process.py +++ b/dynatrace/tenant/topology/process.py @@ -1,3 +1,5 @@ +"""Module for process type entity operations""" + import dynatrace.tenant.topology.shared as entity_api diff --git a/dynatrace/tenant/topology/process_groups.py b/dynatrace/tenant/topology/process_groups.py index 34f242e..fced407 100644 --- a/dynatrace/tenant/topology/process_groups.py +++ b/dynatrace/tenant/topology/process_groups.py @@ -1,3 +1,5 @@ +"""Module for process group type entity operations""" + import dynatrace.tenant.topology.shared as entity_api import dynatrace.requests.request_handler as rh diff --git a/dynatrace/tenant/topology/services.py b/dynatrace/tenant/topology/services.py index 98e8a43..f2db715 100644 --- a/dynatrace/tenant/topology/services.py +++ b/dynatrace/tenant/topology/services.py @@ -1,3 +1,5 @@ +"""Module for service type entity operations""" + import dynatrace.tenant.topology.shared as entity_api import dynatrace.requests.request_handler as rh @@ -71,4 +73,4 @@ def add_service_tags(cluster, tenant, entity, tag_list): tenant=tenant, tag_list=tag_list, entity_id=entity - ) \ No newline at end of file + ) diff --git a/dynatrace/tenant/topology/shared.py b/dynatrace/tenant/topology/shared.py index 4c9645b..aed30c8 100644 --- a/dynatrace/tenant/topology/shared.py +++ b/dynatrace/tenant/topology/shared.py @@ -1,3 +1,5 @@ +"""Module for core entity operations""" + from enum import Enum, auto from dynatrace.requests import request_handler as rh @@ -151,8 +153,8 @@ def get_entity(cluster, tenant, entity_id, params=None): if len(response.json().get('entities')) == 1: return response.json().get('entities')[0] - else: - return response.json().get('entities') + + return response.json().get('entities') def get_env_entity_count(cluster, tenant, entity_type, params=None): @@ -236,9 +238,9 @@ def add_tags(cluster, tenant, tag_list, entity_type=None, entity_id=None, params # Sanity checking, error handling if not tag_list: raise TypeError("No tags provided") - elif not isinstance(tag_list, list): + if not isinstance(tag_list, list): raise TypeError("tags_list is not a list") - elif not any([entity_type, entity_id]): + if not any([entity_type, entity_id]): raise ValueError("Must specifiy at least either entity_type or entity_id") # Params may already contain an entitySelector, we mustn't overwrite @@ -288,7 +290,7 @@ def delete_tag(cluster, tenant, tag_key, entity_type=None, entity_id=None, # Sanity checking, error handling if not tag_key: raise TypeError("No tag key provided") - elif not any([entity_type, entity_id]): + if not any([entity_type, entity_id]): raise ValueError("Must specifiy at least either entity_type or entity_id") # Params may already contain an entitySelector, we mustn't overwrite From af8f461a87df2cac222c367aa6d07af02c84da99 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 06:36:23 +0100 Subject: [PATCH 073/204] fixed issues from testing and linting --- dynatrace/tenant/topology/hosts.py | 7 ++++--- dynatrace/tenant/topology/shared.py | 8 ++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/dynatrace/tenant/topology/hosts.py b/dynatrace/tenant/topology/hosts.py index f187ca1..bf3c843 100644 --- a/dynatrace/tenant/topology/hosts.py +++ b/dynatrace/tenant/topology/hosts.py @@ -101,10 +101,11 @@ def get_host_units_tenantwide(cluster, tenant, params=None): """ consumed_host_units = 0 host_list = rh.make_api_call( - cluster, - tenant, + cluster=cluster, + tenant=tenant, endpoint=f'{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts', - params=params) + params=params + ).json() for host in host_list: consumed_host_units += host['consumedHostUnits'] return consumed_host_units diff --git a/dynatrace/tenant/topology/shared.py b/dynatrace/tenant/topology/shared.py index aed30c8..c50590d 100644 --- a/dynatrace/tenant/topology/shared.py +++ b/dynatrace/tenant/topology/shared.py @@ -104,10 +104,10 @@ class EntityTypes(Enum): CUSTOM_DEVICE_GROUP = auto() def __str__(self): - return self.name + return str(self.name) def __repr__(self): - return self.name + return str(self.name) def get_entities(cluster, tenant, entity_type, params=None): @@ -140,9 +140,9 @@ def get_entity(cluster, tenant, entity_id, params=None): # If params already contains entitySelector, don't overwrite if params.get('entitySelector'): - params['entitySelector'] += f'entityId("{entity_id}")' + params['entitySelector'] += f'entityId({entity_id})' else: - params['entitySelector'] = f'entityId("{entity_id}")' + params['entitySelector'] = f'entityId({entity_id})' response = rh.make_api_call( cluster=cluster, From ccee844037e89b64a8a27efaf1ba006595266dcc Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 06:36:50 +0100 Subject: [PATCH 074/204] updated hosts test suite --- .../responses/hosts/get_all.json | 32 ++++++++------- .../responses/hosts/get_single.json | 10 ++++- .../responses/hosts/v1_get_all.json | 14 +++++++ tests/test_topology_hosts.py | 41 +++++++++++++------ tests/tooling_for_test.py | 11 ++++- 5 files changed, 77 insertions(+), 31 deletions(-) create mode 100644 tests/mockserver_payloads/responses/hosts/v1_get_all.json diff --git a/tests/mockserver_payloads/responses/hosts/get_all.json b/tests/mockserver_payloads/responses/hosts/get_all.json index bae3691..eb816cf 100644 --- a/tests/mockserver_payloads/responses/hosts/get_all.json +++ b/tests/mockserver_payloads/responses/hosts/get_all.json @@ -1,14 +1,18 @@ -[ - { - "entityId": "HOST-ABC123DEF456GHIJ", - "consumedHostUnits": 0.25 - }, - { - "entityId": "HOST-5B9CE4E4E14185FA", - "consumedHostUnits": 0.25 - }, - { - "entityId": "HOST-421D60DB4A2EA929", - "consumedHostUnits": 3.5 - } -] \ No newline at end of file +{ + "totalCount": 3, + "pageSize": 50, + "entities": [ + { + "entityId": "HOST-ABC123DEF456GHIJ", + "consumedHostUnits": 0.25 + }, + { + "entityId": "HOST-5B9CE4E4E14185FA", + "consumedHostUnits": 0.25 + }, + { + "entityId": "HOST-421D60DB4A2EA929", + "consumedHostUnits": 3.5 + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/hosts/get_single.json b/tests/mockserver_payloads/responses/hosts/get_single.json index b27e2e7..92d8082 100644 --- a/tests/mockserver_payloads/responses/hosts/get_single.json +++ b/tests/mockserver_payloads/responses/hosts/get_single.json @@ -1,4 +1,10 @@ { - "entityId": "HOST-ABC123DEF456GHIJ", - "consumedHostUnits": 0.25 + "totalCount": 1, + "pageSize": 50, + "entities": [ + { + "entityId": "HOST-ABC123DEF456GHIJ", + "consumedHostUnits": 0.25 + } + ] } \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/hosts/v1_get_all.json b/tests/mockserver_payloads/responses/hosts/v1_get_all.json new file mode 100644 index 0000000..8130b93 --- /dev/null +++ b/tests/mockserver_payloads/responses/hosts/v1_get_all.json @@ -0,0 +1,14 @@ +[ + { + "entityId": "HOST-ABC123DEF456GHIJ", + "consumedHostUnits": 0.25 + }, + { + "entityId": "HOST-5B9CE4E4E14185FA", + "consumedHostUnits": 0.25 + }, + { + "entityId": "HOST-421D60DB4A2EA929", + "consumedHostUnits": 3.5 + } +] \ No newline at end of file diff --git a/tests/test_topology_hosts.py b/tests/test_topology_hosts.py index c25af78..be70719 100644 --- a/tests/test_topology_hosts.py +++ b/tests/test_topology_hosts.py @@ -2,14 +2,16 @@ Test Suite for Topology Hosts """ import unittest -from user_variables import FULL_SET # pylint: disable=import-error +from variable_sets.radu_vars import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import hosts CLUSTER = FULL_SET["mockserver1"] TENANT = "tenant1" -URL_PATH = f"{TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts" +URL_PATH = str(TenantAPIs.ENTITIES) +V1_URL_PATH = f'{TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts' +TAG_URL_PATH = str(TenantAPIs.TAGS) REQUEST_DIR = "tests/mockserver_payloads/requests/hosts" RESPONSE_DIR = "tests/mockserver_payloads/responses/hosts" @@ -27,28 +29,36 @@ def test_get_all_hosts(self): tenant=TENANT, url_path=URL_PATH, request_type="GET", + parameters={ + 'entitySelector': 'type("HOST")' + }, response_file=response_file ) result = hosts.get_hosts_tenantwide(CLUSTER, TENANT) - self.assertEqual(result, testtools.expected_payload(response_file)) + expected_result = testtools.expected_payload(response_file).get('entities') + self.assertEqual(result, expected_result) def test_get_single_host(self): """Test fetching a specific host""" - host_id = "HOST-9F74450267BAAE20" + host_id = "HOST-ABC123DEF456GHIJ" response_file = f"{RESPONSE_DIR}/get_single.json" testtools.create_mockserver_expectation( cluster=CLUSTER, tenant=TENANT, - url_path=f"{URL_PATH}/{host_id}", + url_path=URL_PATH, request_type="GET", + parameters={ + 'entitySelector': f'entityId({host_id})' + }, response_file=response_file ) result = hosts.get_host(CLUSTER, TENANT, host_id) - self.assertEqual(result, testtools.expected_payload(response_file)) + expected_result = testtools.expected_payload(response_file).get('entities')[0] + self.assertEqual(result, expected_result) def test_get_host_count(self): """Test getting the count of hosts in a tenant.""" @@ -60,8 +70,10 @@ def test_get_host_count(self): url_path=URL_PATH, request_type="GET", response_file=response_file, - parameters=dict(relativeTime=['day'], - includeDetails=['False']) + parameters={ + 'from': 'now-24h', + 'entitySelector': 'type("HOST")' + } ) result = hosts.get_host_count_tenantwide(CLUSTER, TENANT) @@ -70,11 +82,11 @@ def test_get_host_count(self): def test_get_host_units(self): """Tests getting the consumed host units in a tenant.""" - response_file = f"{RESPONSE_DIR}/get_all.json" + response_file = f"{RESPONSE_DIR}/v1_get_all.json" testtools.create_mockserver_expectation( cluster=CLUSTER, tenant=TENANT, - url_path=URL_PATH, + url_path=V1_URL_PATH, request_type="GET", response_file=response_file ) @@ -97,13 +109,16 @@ def test_add_tags(self): cluster=CLUSTER, tenant=TENANT, request_type="POST", - url_path=f"{URL_PATH}/{host_id}", + url_path=TAG_URL_PATH, request_file=request_file, + parameters={ + 'entitySelector': f'entityId({host_id})' + }, response_code=201 ) result = hosts.add_host_tags(CLUSTER, TENANT, host_id, tags) - self.assertEqual(result, 201) + self.assertEqual(result.status_code, 201) def test_delete_tags(self): """Test deleting a tag from a specific host.""" @@ -114,7 +129,7 @@ def test_delete_tags(self): testtools.create_mockserver_expectation( cluster=CLUSTER, tenant=TENANT, - url_path=f"{URL_PATH}/{host_id}/tags/{tag}", + url_path=TAG_URL_PATH, request_type="DELETE", response_code=204 ) diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index 63b2275..b871399 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -40,9 +40,16 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa logging.debug("URL PATH: %s", url_path) logging.debug("KWARGS %s", kwargs) - # Paramaters should always at least have Api-Token + + # Mockserver expectation syntax expects each parameter's matching values + # to be given as a list (even if just 1 value) if 'parameters' in kwargs: - expectation["httpRequest"]["queryStringParameters"] = kwargs['parameters'] + expectation["httpRequest"]["queryStringParameters"] = { + param: [ + kwargs['parameters'][param] + ] + for param in kwargs['parameters'] + } if "request_file" in kwargs: with open(kwargs['request_file']) as open_file: From b30f32a712b1024b21b600f9e5d3aab8c76a637c Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 06:48:46 +0100 Subject: [PATCH 075/204] updated process group test suite --- .../responses/processes/get_all_pgis.json | 27 +++++++++------- .../responses/processes/get_all_pgs.json | 26 ++++++++------- .../responses/processes/get_one_pg.json | 8 ++++- .../responses/processes/get_one_pgi.json | 8 ++++- tests/test_topology_process_groups.py | 32 ++++++++++++++----- 5 files changed, 69 insertions(+), 32 deletions(-) diff --git a/tests/mockserver_payloads/responses/processes/get_all_pgis.json b/tests/mockserver_payloads/responses/processes/get_all_pgis.json index 121da81..05964fe 100644 --- a/tests/mockserver_payloads/responses/processes/get_all_pgis.json +++ b/tests/mockserver_payloads/responses/processes/get_all_pgis.json @@ -1,11 +1,16 @@ -[ - { - "entityId": "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" - }, - { - "entityId": "PROCESS_GROUP_INSTANCE-A6AAFEA17E6F60FD" - }, - { - "entityId": "PROCESS_GROUP_INSTANCE-F0967E6BFEE20424" - } -] \ No newline at end of file +{ + "totalCount": 3, + "pageSize": 50, + "entities": + [ + { + "entityId": "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" + }, + { + "entityId": "PROCESS_GROUP_INSTANCE-A6AAFEA17E6F60FD" + }, + { + "entityId": "PROCESS_GROUP_INSTANCE-F0967E6BFEE20424" + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_all_pgs.json b/tests/mockserver_payloads/responses/processes/get_all_pgs.json index 73e086a..08464d2 100644 --- a/tests/mockserver_payloads/responses/processes/get_all_pgs.json +++ b/tests/mockserver_payloads/responses/processes/get_all_pgs.json @@ -1,11 +1,15 @@ -[ - { - "entityId": "PROCESS_GROUP-ABC123DEF456GHI7" - }, - { - "entityId": "PROCESS_GROUP-19DACA5E22637C33" - }, - { - "entityId": "PROCESS_GROUP-859E1549052CD876" - } -] \ No newline at end of file +{ + "totalCount": 3, + "pageSize": 50, + "entities": [ + { + "entityId": "PROCESS_GROUP-ABC123DEF456GHI7" + }, + { + "entityId": "PROCESS_GROUP-19DACA5E22637C33" + }, + { + "entityId": "PROCESS_GROUP-859E1549052CD876" + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_one_pg.json b/tests/mockserver_payloads/responses/processes/get_one_pg.json index 14223a1..5bb1a49 100644 --- a/tests/mockserver_payloads/responses/processes/get_one_pg.json +++ b/tests/mockserver_payloads/responses/processes/get_one_pg.json @@ -1,3 +1,9 @@ { - "entityId": "PROCESS_GROUP-ABC123DEF456GHI7" + "totalCount": 3, + "pageSize": 50, + "entities": [ + { + "entityId": "PROCESS_GROUP-ABC123DEF456GHI7" + } + ] } \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_one_pgi.json b/tests/mockserver_payloads/responses/processes/get_one_pgi.json index 0898df1..63bee60 100644 --- a/tests/mockserver_payloads/responses/processes/get_one_pgi.json +++ b/tests/mockserver_payloads/responses/processes/get_one_pgi.json @@ -1,3 +1,9 @@ { - "entityId": "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" + "totalCount": 3, + "pageSize": 50, + "entities": [ + { + "entityId": "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" + } + ] } \ No newline at end of file diff --git a/tests/test_topology_process_groups.py b/tests/test_topology_process_groups.py index 0d6c099..b21d435 100644 --- a/tests/test_topology_process_groups.py +++ b/tests/test_topology_process_groups.py @@ -3,12 +3,15 @@ import unittest from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools +from dynatrace.tenant.topology.shared import EntityTypes from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import process_groups CLUSTER = FULL_SET.get('mockserver1') TENANT = 'tenant1' -URL_PATH = f"{TenantAPIs.V1_TOPOLOGY}/infrastructure/process-groups" +URL_PATH = f"{TenantAPIs.ENTITIES}" +TAG_URL_PATH = f"{TenantAPIs.TAGS}" +ENTITY = f"{EntityTypes.PROCESS_GROUP}" REQUEST_DIR = "tests/mockserver_payloads/requests/processes" RESPONSE_DIR = "tests/mockserver_payloads/responses/processes" @@ -25,11 +28,15 @@ def test_get_all_pgs(self): tenant=TENANT, url_path=URL_PATH, request_type="GET", + parameters={ + 'entitySelector': f'type("{ENTITY}")' + }, response_file=response_file ) result = process_groups.get_process_groups_tenantwide(CLUSTER, TENANT) - self.assertEqual(result, testtools.expected_payload(response_file)) + expected_result = testtools.expected_payload(response_file).get('entities') + self.assertEqual(result, expected_result) def test_get_single_pg(self): """Test fetching single PG""" @@ -39,13 +46,17 @@ def test_get_single_pg(self): testtools.create_mockserver_expectation( cluster=CLUSTER, tenant=TENANT, - url_path=f"{URL_PATH}/{pg_id}", + url_path=URL_PATH, request_type="GET", + parameters={ + 'entitySelector': f'entityId({pg_id})' + }, response_file=response_file ) result = process_groups.get_process_group(CLUSTER, TENANT, pg_id) - self.assertEqual(result, testtools.expected_payload(response_file)) + expected_result = testtools.expected_payload(response_file).get('entities')[0] + self.assertEqual(result, expected_result) def test_get_pg_count(self): """Test getting the PG count tenantwide.""" @@ -56,11 +67,13 @@ def test_get_pg_count(self): tenant=TENANT, url_path=URL_PATH, request_type="GET", + parameters={ + 'entitySelector': f'type("{ENTITY}")' + }, response_file=response_file ) - result = process_groups.get_process_group_count_tenantwide(CLUSTER, - TENANT) + result = process_groups.get_process_group_count_tenantwide(CLUSTER, TENANT) self.assertEqual(result, 3) @@ -77,14 +90,17 @@ def test_add_pg_tags(self): cluster=CLUSTER, tenant=TENANT, request_type="POST", - url_path=f"{URL_PATH}/{pg_id}", + url_path=TAG_URL_PATH, request_file=request_file, + parameters={ + 'entitySelector': f'entityId({pg_id})' + }, response_code=201 ) result = process_groups.add_process_group_tags(CLUSTER, TENANT, pg_id, tags) - self.assertEqual(result, 201) + self.assertEqual(result.status_code, 201) if __name__ == '__main__': From ea07d675decc638c30f0a46f621387842ba0dd05 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 06:49:41 +0100 Subject: [PATCH 076/204] changed variables back to user_variables --- tests/test_topology_hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_topology_hosts.py b/tests/test_topology_hosts.py index be70719..7be11ec 100644 --- a/tests/test_topology_hosts.py +++ b/tests/test_topology_hosts.py @@ -2,7 +2,7 @@ Test Suite for Topology Hosts """ import unittest -from variable_sets.radu_vars import FULL_SET # pylint: disable=import-error +from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import hosts From a407d672f07745689376ba0e78b86056a35cc611 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 07:02:53 +0100 Subject: [PATCH 077/204] updated mockserver payloads --- .../responses/processes/get_one_pg.json | 2 +- .../responses/processes/get_one_pgi.json | 2 +- .../responses/services/get_all.json | 26 +++++++++++-------- .../responses/services/get_one.json | 8 +++++- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/tests/mockserver_payloads/responses/processes/get_one_pg.json b/tests/mockserver_payloads/responses/processes/get_one_pg.json index 5bb1a49..4fcd720 100644 --- a/tests/mockserver_payloads/responses/processes/get_one_pg.json +++ b/tests/mockserver_payloads/responses/processes/get_one_pg.json @@ -1,5 +1,5 @@ { - "totalCount": 3, + "totalCount": 1, "pageSize": 50, "entities": [ { diff --git a/tests/mockserver_payloads/responses/processes/get_one_pgi.json b/tests/mockserver_payloads/responses/processes/get_one_pgi.json index 63bee60..43f1e80 100644 --- a/tests/mockserver_payloads/responses/processes/get_one_pgi.json +++ b/tests/mockserver_payloads/responses/processes/get_one_pgi.json @@ -1,5 +1,5 @@ { - "totalCount": 3, + "totalCount": 1, "pageSize": 50, "entities": [ { diff --git a/tests/mockserver_payloads/responses/services/get_all.json b/tests/mockserver_payloads/responses/services/get_all.json index bdcce80..3c21793 100644 --- a/tests/mockserver_payloads/responses/services/get_all.json +++ b/tests/mockserver_payloads/responses/services/get_all.json @@ -1,11 +1,15 @@ -[ - { - "entityId": "SERVICE-ABC123DEF456GHI7" - }, - { - "entityId": "SERVICE-C096CE0BA471AEFD" - }, - { - "entityId": "SERVICE-B71ADA892013D156" - } -] \ No newline at end of file +{ + "totalCount": 3, + "pageSize": 50, + "entities": [ + { + "entityId": "SERVICE-ABC123DEF456GHI7" + }, + { + "entityId": "SERVICE-C096CE0BA471AEFD" + }, + { + "entityId": "SERVICE-B71ADA892013D156" + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/services/get_one.json b/tests/mockserver_payloads/responses/services/get_one.json index f5c0619..0acae91 100644 --- a/tests/mockserver_payloads/responses/services/get_one.json +++ b/tests/mockserver_payloads/responses/services/get_one.json @@ -1,3 +1,9 @@ { - "entityId": "SERVICE-ABC123DEF456GHI7" + "totalCount": 1, + "pageSize": 50, + "entities": [ + { + "entityId": "SERVICE-ABC123DEF456GHI7" + } + ] } \ No newline at end of file From c5744cb59039a559f73878f6cf5d81ae999d1191 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 07:10:16 +0100 Subject: [PATCH 078/204] updated topology.service test suite --- tests/test_topology_services.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/tests/test_topology_services.py b/tests/test_topology_services.py index 115f2ee..f97cd04 100644 --- a/tests/test_topology_services.py +++ b/tests/test_topology_services.py @@ -3,12 +3,15 @@ import unittest from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools +from dynatrace.tenant.topology.shared import EntityTypes from dynatrace.requests.request_handler import TenantAPIs from dynatrace.tenant.topology import services CLUSTER = FULL_SET.get('mockserver1') TENANT = 'tenant1' -URL_PATH = f"{TenantAPIs.V1_TOPOLOGY}/infrastructure/services" +URL_PATH = f"{TenantAPIs.ENTITIES}" +TAG_URL_PATH = f"{TenantAPIs.TAGS}" +TYPE = f"{EntityTypes.SERVICE}" REQUEST_DIR = "tests/mockserver_payloads/requests/services" RESPONSE_DIR = "tests/mockserver_payloads/responses/services" @@ -25,11 +28,15 @@ def test_get_all_svc(self): tenant=TENANT, url_path=URL_PATH, request_type="GET", + parameters={ + 'entitySelector': f'type("{TYPE}")' + }, response_file=response_file ) result = services.get_services_tenantwide(CLUSTER, TENANT) - self.assertEqual(result, testtools.expected_payload(response_file)) + expected_result = testtools.expected_payload(response_file).get('entities') + self.assertEqual(result, expected_result) def test_get_single_svc(self): """Test fetching single service""" @@ -39,13 +46,17 @@ def test_get_single_svc(self): testtools.create_mockserver_expectation( cluster=CLUSTER, tenant=TENANT, - url_path=f"{URL_PATH}/{svc_id}", + url_path=URL_PATH, request_type="GET", + parameters={ + 'entitySelector': f'entityId({svc_id})' + }, response_file=response_file ) result = services.get_service(CLUSTER, TENANT, svc_id) - self.assertEqual(result, testtools.expected_payload(response_file)) + expected_result = testtools.expected_payload(response_file).get('entities')[0] + self.assertEqual(result, expected_result) def test_get_svc_count(self): """Test getting the service count tenantwide.""" @@ -56,6 +67,10 @@ def test_get_svc_count(self): tenant=TENANT, url_path=URL_PATH, request_type="GET", + parameters={ + 'from': 'now-24h', + 'entitySelector': f'type("{TYPE}")' + }, response_file=response_file ) @@ -76,13 +91,16 @@ def test_add_svc_tags(self): cluster=CLUSTER, tenant=TENANT, request_type="POST", - url_path=f"{URL_PATH}/{svc_id}", + url_path=TAG_URL_PATH, request_file=request_file, + parameters={ + 'entitySelector': f'entityId({svc_id})' + }, response_code=201 ) result = services.add_service_tags(CLUSTER, TENANT, svc_id, tags) - self.assertEqual(result, 201) + self.assertEqual(result.status_code, 201) if __name__ == '__main__': From bd06dfa158baa918b803f0948ec0d44b58efd239 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 07:18:15 +0100 Subject: [PATCH 079/204] updated topology.processes test cases --- tests/test_topology_processes.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/test_topology_processes.py b/tests/test_topology_processes.py index e9c48a9..4e9aacf 100644 --- a/tests/test_topology_processes.py +++ b/tests/test_topology_processes.py @@ -4,12 +4,14 @@ from tests import tooling_for_test as testtools from dynatrace import settings from dynatrace.requests.request_handler import TenantAPIs +from dynatrace.tenant.topology.shared import EntityTypes from dynatrace.tenant.topology import process FULL_SET = settings.get_setting("FULL_SET") CLUSTER = FULL_SET.get('mockserver1') TENANT = 'tenant1' -URL_PATH = f"{TenantAPIs.V1_TOPOLOGY}/infrastructure/processes" +URL_PATH = f"{TenantAPIs.ENTITIES}" +TYPE = f"{EntityTypes.PROCESS_GROUP_INSTANCE}" RESPONSE_DIR = "tests/mockserver_payloads/responses/processes" @@ -25,11 +27,15 @@ def test_get_all_processes(self): tenant=TENANT, url_path=URL_PATH, request_type="GET", + parameters={ + 'entitySelector': f'type("{TYPE}")' + }, response_file=response_file ) result = process.get_processes_tenantwide(CLUSTER, TENANT) - self.assertEqual(result, testtools.expected_payload(response_file)) + expected_result = testtools.expected_payload(response_file).get('entities') + self.assertEqual(result, expected_result) def test_get_single_process(self): """Tests getting one specific process.""" @@ -39,13 +45,17 @@ def test_get_single_process(self): testtools.create_mockserver_expectation( cluster=CLUSTER, tenant=TENANT, - url_path=f"{URL_PATH}/{process_id}", + url_path=URL_PATH, request_type="GET", + parameters={ + 'entitySelector': f'entityId({process_id})' + }, response_file=response_file ) result = process.get_process(CLUSTER, TENANT, process_id) - self.assertEqual(result, testtools.expected_payload(response_file)) + expected_result = testtools.expected_payload(response_file).get('entities')[0] + self.assertEqual(result, expected_result) if __name__ == '__main__': From 4c8fcd71c16e505e3585ae808ae98b6704fe222d Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 07:24:46 +0100 Subject: [PATCH 080/204] updated host_groups test case to comply with test_tooling --- tests/test_host_groups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_host_groups.py b/tests/test_host_groups.py index 9d3c4ae..4d6d172 100644 --- a/tests/test_host_groups.py +++ b/tests/test_host_groups.py @@ -16,8 +16,8 @@ class TestHostGroupFunctions(unittest.TestCase): def test_get_host_groups_tenantwide(self): """Testing Retreival of all Host Groups within a single tenant""" parameters = { - "relativeTime": ["day"], - "includeDetails": ["true"], + "relativeTime": "day", + "includeDetails": "true", } mockserver_response_file = f"{self.RESPONSE_DIR}mock_get_general_1.json" tooling_for_test.create_mockserver_expectation( From cd7a0fbdb99d42dee7cba8c2612fa39e11c647ea Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 08:00:48 +0100 Subject: [PATCH 081/204] updated host_group test suite --- .../host_groups/mock_get_general_1.json | 48 ++++++------------- tests/test_host_groups.py | 20 ++++---- 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/tests/mockserver_payloads/responses/host_groups/mock_get_general_1.json b/tests/mockserver_payloads/responses/host_groups/mock_get_general_1.json index 2b383c5..6524813 100644 --- a/tests/mockserver_payloads/responses/host_groups/mock_get_general_1.json +++ b/tests/mockserver_payloads/responses/host_groups/mock_get_general_1.json @@ -1,34 +1,14 @@ -[{ - "entityId": "HOST-238441A17F95B305", - "displayName": "testserver", - "discoveredName": "testserver", - "firstSeenTimestamp": 1592513300463, - "lastSeenTimestamp": 1592980597441, - "tags": [], - "fromRelationships": {}, - "toRelationships": { - "isProcessOf": [], - "runsOn": [] - }, - "osType": "LINUX", - "osArchitecture": "X86", - "osVersion": "Debian GNU/Linux 10 (buster) (kernel 4.19.0-9-amd64)", - "bitness": "64bit", - "cpuCores": 1, - "logicalCpuCores": 2, - "monitoringMode": "FULL_STACK", - "networkZoneId": "default", - "agentVersion": { - "major": 1, - "minor": 195, - "revision": 54, - "timestamp": "20200529-113801", - "sourceRevision": "" - }, - "consumedHostUnits": 8.0, - "userLevel": "SUPERUSER", - "hostGroup": { - "meId": "HOST_GROUP-ABCDEFGH12345678", - "name": "HOST_GROUP_1" - } -}] \ No newline at end of file +{ + "totalCount": 2, + "pageSize": 50, + "entities": [ + { + "entityId": "HOST_GROUP-ABC123DEF456GHI7", + "displayName": "TEST_HOSTGROUP_1" + }, + { + "entityId": "HOST_GROUP-DEF345GHI678JKL9", + "displayName": "TEST_HOSTGROUP_2" + } + ] +} \ No newline at end of file diff --git a/tests/test_host_groups.py b/tests/test_host_groups.py index 4d6d172..c1561ca 100644 --- a/tests/test_host_groups.py +++ b/tests/test_host_groups.py @@ -1,12 +1,15 @@ """Testing dynatrace.tenant.host_groups""" import unittest import user_variables # pylint: disable=import-error -from tests import tooling_for_test +from tests import tooling_for_test as testtools +from dynatrace.requests.request_handler import TenantAPIs +from dynatrace.tenant.topology.shared import EntityTypes from dynatrace.tenant import host_groups CLUSTER = user_variables.FULL_SET["mockserver1"] TENANT = "tenant1" -URL_PATH = "/api/v1/entity/infrastructure/hosts" +URL_PATH = f"{TenantAPIs.ENTITIES}" +TYPE = f"{EntityTypes.HOST_GROUP}" class TestHostGroupFunctions(unittest.TestCase): @@ -16,23 +19,24 @@ class TestHostGroupFunctions(unittest.TestCase): def test_get_host_groups_tenantwide(self): """Testing Retreival of all Host Groups within a single tenant""" parameters = { - "relativeTime": "day", - "includeDetails": "true", + "from": "now-24h", + "entitySelector": f'type("{TYPE}")', } - mockserver_response_file = f"{self.RESPONSE_DIR}mock_get_general_1.json" - tooling_for_test.create_mockserver_expectation( + response_file = f"{self.RESPONSE_DIR}mock_get_general_1.json" + testtools.create_mockserver_expectation( CLUSTER, TENANT, URL_PATH, "GET", parameters=parameters, - response_file=mockserver_response_file + response_file=response_file ) command_tested = host_groups.get_host_groups_tenantwide( CLUSTER, TENANT) expected_result = { - 'HOST_GROUP-ABCDEFGH12345678': 'HOST_GROUP_1' + hg.get('entityId'): hg.get('displayName') + for hg in testtools.expected_payload(response_file).get('entities') } self.assertEqual(command_tested, expected_result) From 16237ad673f46806f29599de88e8ff3280d052c0 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 08:01:16 +0100 Subject: [PATCH 082/204] updated host_groups module to use the new topology.shared functions --- dynatrace/tenant/host_groups.py | 73 +++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/dynatrace/tenant/host_groups.py b/dynatrace/tenant/host_groups.py index 968acff..9d6f0a8 100644 --- a/dynatrace/tenant/host_groups.py +++ b/dynatrace/tenant/host_groups.py @@ -1,5 +1,5 @@ -"""Host Group Information for Tenant""" -from dynatrace.tenant.topology import hosts as topology_hosts +"""Module for host group type entity operations""" +import dynatrace.tenant.topology.shared as entity_api # TODO redo export function (break out to export function?) # def export_host_groups_setwide(full_set): @@ -21,49 +21,68 @@ def get_host_groups_tenantwide(cluster, tenant): Returns: Dict: List of Host Groups in the tenant """ - params = { - 'relativeTime': 'day', - 'includeDetails': 'true' + response = entity_api.get_entities( + cluster=cluster, + tenant=tenant, + entity_type=entity_api.EntityTypes.HOST_GROUP, + params={ + 'from': 'now-24h' + } + ) + host_groups = { + hg.get('entityId'): hg.get('displayName') + for hg in response } - response = topology_hosts.get_hosts_tenantwide(cluster, - tenant, - params=params) - host_groups = {} - for host in response: - if host.get('hostGroup'): - host_groups[host['hostGroup']['meId']] = host['hostGroup']['name'] return host_groups -def get_host_groups_clusterwide(cluster): +def get_host_groups_clusterwide(cluster, tenant_split=False): """Get all Host Groups used in the Cluster Args: cluster (cluster dict): Current cluster to operate on + tenant_split (bool): whether to split results by tenant or not Returns: - dict: Dictionary of all Host Groups used in the Cluster + dict: Dictionary of all Host Groups used in the Cluster. + If split by tenant, keys are tenants, values are dicts of + tenant's host groups. """ - # TODO add split_by_tenant optional variable - host_groups_custerwide = {} - for tenant in cluster['tenant']: - host_groups_custerwide.update( - get_host_groups_tenantwide(cluster, tenant) - ) - return host_groups_custerwide + host_groups = {} + + if tenant_split: + for tenant in cluster['tenant']: + host_groups[tenant] = get_host_groups_tenantwide(cluster, tenant) + else: + for tenant in cluster['tenant']: + host_groups.update( + get_host_groups_tenantwide(cluster, tenant) + ) + + return host_groups -def get_host_groups_setwide(full_set): - # TODO add split_by_tenant optional variable +def get_host_groups_setwide(full_set, tenant_split=False): """Get all Host Groups used in the Cluster Set Args: full_set (dict of cluster dict): Current cluster to operate on + tenant_split (bool): whether to split results by tenant or not Returns: dict: Dictionary of all Host Groups used in the Cluster Set + If split by tenant, keys are tenants, values are dicts of + tenant's host groups. """ - host_groups_setwide = {} - for cluster in full_set.values(): - host_groups_setwide.update(get_host_groups_clusterwide(cluster)) - return host_groups_setwide + host_groups = {} + + if tenant_split: + for cluster in full_set.values(): + host_groups.update(get_host_groups_clusterwide( + cluster=cluster, + tenant_split=True)) + else: + for cluster in full_set.values(): + host_groups.update(get_host_groups_clusterwide(cluster)) + + return host_groups From 1b2da03073ce9c1777a2297a6cb39b299dbbdd8d Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 15:19:11 +0100 Subject: [PATCH 083/204] #PAF-51: topology.shared uses paginated methods. added func for getting entities by page. --- dynatrace/tenant/topology/shared.py | 33 ++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/dynatrace/tenant/topology/shared.py b/dynatrace/tenant/topology/shared.py index c50590d..fa79ed3 100644 --- a/dynatrace/tenant/topology/shared.py +++ b/dynatrace/tenant/topology/shared.py @@ -121,15 +121,41 @@ def get_entities(cluster, tenant, entity_type, params=None): else: params['entitySelector'] = f'type("{entity_type}")' - response = rh.make_api_call( + response = rh.v2_get_results_whole( cluster=cluster, tenant=tenant, + item='entities', endpoint=rh.TenantAPIs.ENTITIES, params=params ) return response.json().get('entities') +def get_entities_by_page(cluster, tenant, entity_type, params=None): + """Get all Entities of a given type, page by page. + Returns a generator, page by page. + """ + if not params: + params = {} + + # If params already contains entitySelector, don't overwrite + if params.get('entitySelector'): + params['entitySelector'] += f'type("{entity_type}")' + else: + params['entitySelector'] = f'type("{entity_type}")' + + response = rh.v2_get_results_by_page( + cluster=cluster, + tenant=tenant, + endpoint=rh.TenantAPIs.ENTITIES, + item='entities', + params=params + ) + + for entity in response: + yield entity + + def get_entity(cluster, tenant, entity_id, params=None): """ Get the details of an entity specified by ID. @@ -144,14 +170,15 @@ def get_entity(cluster, tenant, entity_id, params=None): else: params['entitySelector'] = f'entityId({entity_id})' - response = rh.make_api_call( + response = rh.v2_get_results_whole( cluster=cluster, tenant=tenant, endpoint=rh.TenantAPIs.ENTITIES, + item='entities', params=params ) - if len(response.json().get('entities')) == 1: + if response.get('totalCount') == 1: return response.json().get('entities')[0] return response.json().get('entities') From 2137442c95b7d7d3e448ce84dcf598356a29ff6c Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 17:47:24 +0100 Subject: [PATCH 084/204] #PAF52 - Refactored everything into new folder structure --- dynatrace/cluster/config.py | 2 +- dynatrace/cluster/ssl_cert.py | 2 +- dynatrace/cluster/sso.py | 2 +- dynatrace/cluster/user_groups.py | 2 +- dynatrace/cluster/users.py | 4 ++-- dynatrace/{ => framework}/exceptions.py | 0 dynatrace/{requests => framework}/request_handler.py | 0 dynatrace/{ => framework}/settings.py | 0 dynatrace/tenant/{topology => }/applications.py | 4 ++-- dynatrace/tenant/{topology => }/custom.py | 2 +- dynatrace/tenant/extensions.py | 2 +- dynatrace/tenant/host_groups.py | 2 +- dynatrace/tenant/{topology => }/hosts.py | 4 ++-- dynatrace/tenant/maintenance.py | 4 ++-- dynatrace/tenant/management_zones.py | 2 +- dynatrace/tenant/metrics.py | 2 +- dynatrace/tenant/{topology => }/process.py | 2 +- dynatrace/tenant/{topology => }/process_groups.py | 4 ++-- dynatrace/tenant/request_attributes.py | 2 +- dynatrace/tenant/request_naming.py | 2 +- dynatrace/tenant/{topology => }/services.py | 4 ++-- dynatrace/tenant/{topology => }/shared.py | 2 +- dynatrace/tenant/timeseries.py | 2 +- dynatrace/tenant/topology/__init__.py | 0 tests/test_host_groups.py | 4 ++-- tests/test_maintenance_windows.py | 4 ++-- tests/test_settings.py | 2 +- tests/test_topology_hosts.py | 4 ++-- tests/test_topology_process_groups.py | 6 +++--- tests/test_topology_processes.py | 8 ++++---- tests/test_topology_services.py | 6 +++--- tests/tooling_for_test.py | 2 +- 32 files changed, 44 insertions(+), 44 deletions(-) rename dynatrace/{ => framework}/exceptions.py (100%) rename dynatrace/{requests => framework}/request_handler.py (100%) rename dynatrace/{ => framework}/settings.py (100%) rename dynatrace/tenant/{topology => }/applications.py (95%) rename dynatrace/tenant/{topology => }/custom.py (90%) rename dynatrace/tenant/{topology => }/hosts.py (97%) rename dynatrace/tenant/{topology => }/process.py (91%) rename dynatrace/tenant/{topology => }/process_groups.py (95%) rename dynatrace/tenant/{topology => }/services.py (95%) rename dynatrace/tenant/{topology => }/shared.py (99%) delete mode 100644 dynatrace/tenant/topology/__init__.py diff --git a/dynatrace/cluster/config.py b/dynatrace/cluster/config.py index f4040f5..6c08093 100644 --- a/dynatrace/cluster/config.py +++ b/dynatrace/cluster/config.py @@ -1,5 +1,5 @@ """Cluster Config Operations for Dynatrace Managed""" -import dynatrace.requests.request_handler as rh +import dynatrace.framework.request_handler as rh def get_node_info(cluster): diff --git a/dynatrace/cluster/ssl_cert.py b/dynatrace/cluster/ssl_cert.py index b09e4e4..7f50088 100644 --- a/dynatrace/cluster/ssl_cert.py +++ b/dynatrace/cluster/ssl_cert.py @@ -1,6 +1,6 @@ #!/bin/python3 """Cluster SSL Certificate Operations""" -import dynatrace.requests.request_handler as rh +import dynatrace.framework.request_handler as rh def get_cert_details(cluster, entity_type, entity_id): diff --git a/dynatrace/cluster/sso.py b/dynatrace/cluster/sso.py index ab881ed..efd8816 100644 --- a/dynatrace/cluster/sso.py +++ b/dynatrace/cluster/sso.py @@ -1,5 +1,5 @@ """SSO Operations for Dynatrace""" -import dynatrace.requests.request_handler as rh +import dynatrace.framework.request_handler as rh ENDPOINT = "/api/v1.0/onpremise/sso/ssoProvider" diff --git a/dynatrace/cluster/user_groups.py b/dynatrace/cluster/user_groups.py index 170467e..6734da0 100644 --- a/dynatrace/cluster/user_groups.py +++ b/dynatrace/cluster/user_groups.py @@ -1,7 +1,7 @@ #!/bin/python3 """Cluster Group Operations""" import user_variables # pylint: disable=import-error -from dynatrace.requests import request_handler as rh +from dynatrace.framework import request_handler as rh from dynatrace.tenant import management_zones as mzh MZ_USER_PERMISSONS = { diff --git a/dynatrace/cluster/users.py b/dynatrace/cluster/users.py index ff1f8a3..621fab8 100644 --- a/dynatrace/cluster/users.py +++ b/dynatrace/cluster/users.py @@ -1,6 +1,6 @@ """User Operations in Cluster Mangement""" -import dynatrace.requests.request_handler as rh -from dynatrace.exceptions import ManagedClusterOnlyException +import dynatrace.framework.request_handler as rh +from dynatrace.framework.exceptions import ManagedClusterOnlyException # TODO add check for is_managed diff --git a/dynatrace/exceptions.py b/dynatrace/framework/exceptions.py similarity index 100% rename from dynatrace/exceptions.py rename to dynatrace/framework/exceptions.py diff --git a/dynatrace/requests/request_handler.py b/dynatrace/framework/request_handler.py similarity index 100% rename from dynatrace/requests/request_handler.py rename to dynatrace/framework/request_handler.py diff --git a/dynatrace/settings.py b/dynatrace/framework/settings.py similarity index 100% rename from dynatrace/settings.py rename to dynatrace/framework/settings.py diff --git a/dynatrace/tenant/topology/applications.py b/dynatrace/tenant/applications.py similarity index 95% rename from dynatrace/tenant/topology/applications.py rename to dynatrace/tenant/applications.py index 10b20ba..3243794 100644 --- a/dynatrace/tenant/topology/applications.py +++ b/dynatrace/tenant/applications.py @@ -1,7 +1,7 @@ """Module for application type entity operations""" -import dynatrace.tenant.topology.shared as entity_api -import dynatrace.requests.request_handler as rh +import dynatrace.tenant.shared as entity_api +import dynatrace.framework.request_handler as rh def get_applications_tenantwide(cluster, tenant): diff --git a/dynatrace/tenant/topology/custom.py b/dynatrace/tenant/custom.py similarity index 90% rename from dynatrace/tenant/topology/custom.py rename to dynatrace/tenant/custom.py index ca1edb8..2259850 100644 --- a/dynatrace/tenant/topology/custom.py +++ b/dynatrace/tenant/custom.py @@ -1,6 +1,6 @@ """Module or custom device type entity operations""" -import dynatrace.requests.request_handler as rh +import dynatrace.framework.request_handler as rh def set_custom_properties(cluster, tenant, entity, prop_json): diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index db43fb7..7ad7e09 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -1,5 +1,5 @@ """Operations Interacting with Dynatrace Extensions""" -from dynatrace.requests import request_handler as rh +from dynatrace.framework import request_handler as rh ENDPOINT = rh.TenantAPIs.EXTENSIONS diff --git a/dynatrace/tenant/host_groups.py b/dynatrace/tenant/host_groups.py index 9d6f0a8..c03a5b3 100644 --- a/dynatrace/tenant/host_groups.py +++ b/dynatrace/tenant/host_groups.py @@ -1,5 +1,5 @@ """Module for host group type entity operations""" -import dynatrace.tenant.topology.shared as entity_api +import dynatrace.tenant.shared as entity_api # TODO redo export function (break out to export function?) # def export_host_groups_setwide(full_set): diff --git a/dynatrace/tenant/topology/hosts.py b/dynatrace/tenant/hosts.py similarity index 97% rename from dynatrace/tenant/topology/hosts.py rename to dynatrace/tenant/hosts.py index bf3c843..e052186 100644 --- a/dynatrace/tenant/topology/hosts.py +++ b/dynatrace/tenant/hosts.py @@ -1,7 +1,7 @@ """Module for host type entity operations""" -import dynatrace.tenant.topology.shared as entity_api -import dynatrace.requests.request_handler as rh +import dynatrace.tenant.shared as entity_api +import dynatrace.framework.request_handler as rh def get_hosts_tenantwide(cluster, tenant, params=None): diff --git a/dynatrace/tenant/maintenance.py b/dynatrace/tenant/maintenance.py index 09681bd..e6d4600 100644 --- a/dynatrace/tenant/maintenance.py +++ b/dynatrace/tenant/maintenance.py @@ -2,9 +2,9 @@ import datetime import re from enum import Enum, auto -import dynatrace.requests.request_handler as rh +import dynatrace.framework.request_handler as rh import user_variables -from dynatrace.exceptions import InvalidDateFormatException +from dynatrace.framework.exceptions import InvalidDateFormatException MZ_ENDPOINT = rh.TenantAPIs.MAINTENANCE_WINDOWS diff --git a/dynatrace/tenant/management_zones.py b/dynatrace/tenant/management_zones.py index f336940..dea1246 100644 --- a/dynatrace/tenant/management_zones.py +++ b/dynatrace/tenant/management_zones.py @@ -3,7 +3,7 @@ """Management Zone Operations for Environment""" import copy import json -from dynatrace.requests import request_handler as rh +from dynatrace.framework import request_handler as rh ENDPOINT = rh.TenantAPIs.MANAGEMENT_ZONES diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index 2d65ed4..0687285 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -1,5 +1,5 @@ """Module for interacting with the Metrics API""" -from dynatrace.requests import request_handler as rh +from dynatrace.framework import request_handler as rh ENDPOINT = rh.TenantAPIs.METRICS diff --git a/dynatrace/tenant/topology/process.py b/dynatrace/tenant/process.py similarity index 91% rename from dynatrace/tenant/topology/process.py rename to dynatrace/tenant/process.py index 8b25119..fbf4eeb 100644 --- a/dynatrace/tenant/topology/process.py +++ b/dynatrace/tenant/process.py @@ -1,6 +1,6 @@ """Module for process type entity operations""" -import dynatrace.tenant.topology.shared as entity_api +import dynatrace.tenant.shared as entity_api def get_processes_tenantwide(cluster, tenant, params=None): diff --git a/dynatrace/tenant/topology/process_groups.py b/dynatrace/tenant/process_groups.py similarity index 95% rename from dynatrace/tenant/topology/process_groups.py rename to dynatrace/tenant/process_groups.py index fced407..a06f2dd 100644 --- a/dynatrace/tenant/topology/process_groups.py +++ b/dynatrace/tenant/process_groups.py @@ -1,7 +1,7 @@ """Module for process group type entity operations""" -import dynatrace.tenant.topology.shared as entity_api -import dynatrace.requests.request_handler as rh +import dynatrace.tenant.shared as entity_api +import dynatrace.framework.request_handler as rh def get_process_groups_tenantwide(cluster, tenant): diff --git a/dynatrace/tenant/request_attributes.py b/dynatrace/tenant/request_attributes.py index bf77e56..8071dcb 100644 --- a/dynatrace/tenant/request_attributes.py +++ b/dynatrace/tenant/request_attributes.py @@ -1,7 +1,7 @@ #!/bin/python3 """Request Attributes Operations""" import json -from dynatrace.requests import request_handler as rh +from dynatrace.framework import request_handler as rh ENDPOINT = rh.TenantAPIs.REQUEST_ATTRIBUTES diff --git a/dynatrace/tenant/request_naming.py b/dynatrace/tenant/request_naming.py index 38cb48f..eaa1cbd 100644 --- a/dynatrace/tenant/request_naming.py +++ b/dynatrace/tenant/request_naming.py @@ -3,7 +3,7 @@ import os import json -from dynatrace.requests import request_handler as rh +from dynatrace.framework import request_handler as rh ENDPOINT = rh.TenantAPIs.REQUEST_NAMING diff --git a/dynatrace/tenant/topology/services.py b/dynatrace/tenant/services.py similarity index 95% rename from dynatrace/tenant/topology/services.py rename to dynatrace/tenant/services.py index f2db715..783d919 100644 --- a/dynatrace/tenant/topology/services.py +++ b/dynatrace/tenant/services.py @@ -1,7 +1,7 @@ """Module for service type entity operations""" -import dynatrace.tenant.topology.shared as entity_api -import dynatrace.requests.request_handler as rh +import dynatrace.tenant.shared as entity_api +import dynatrace.framework.request_handler as rh def get_services_tenantwide(cluster, tenant): diff --git a/dynatrace/tenant/topology/shared.py b/dynatrace/tenant/shared.py similarity index 99% rename from dynatrace/tenant/topology/shared.py rename to dynatrace/tenant/shared.py index fa79ed3..d02801f 100644 --- a/dynatrace/tenant/topology/shared.py +++ b/dynatrace/tenant/shared.py @@ -1,7 +1,7 @@ """Module for core entity operations""" from enum import Enum, auto -from dynatrace.requests import request_handler as rh +from dynatrace.framework import request_handler as rh class EntityTypes(Enum): diff --git a/dynatrace/tenant/timeseries.py b/dynatrace/tenant/timeseries.py index 89873b0..a9f74c4 100644 --- a/dynatrace/tenant/timeseries.py +++ b/dynatrace/tenant/timeseries.py @@ -1,5 +1,5 @@ """Timerseries Operations from Environment V1 API""" -from dynatrace.requests import request_handler as rh +from dynatrace.framework import request_handler as rh ENDPOINT = "timeseries/" diff --git a/dynatrace/tenant/topology/__init__.py b/dynatrace/tenant/topology/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/test_host_groups.py b/tests/test_host_groups.py index c1561ca..8c541d6 100644 --- a/tests/test_host_groups.py +++ b/tests/test_host_groups.py @@ -2,8 +2,8 @@ import unittest import user_variables # pylint: disable=import-error from tests import tooling_for_test as testtools -from dynatrace.requests.request_handler import TenantAPIs -from dynatrace.tenant.topology.shared import EntityTypes +from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.tenant.shared import EntityTypes from dynatrace.tenant import host_groups CLUSTER = user_variables.FULL_SET["mockserver1"] diff --git a/tests/test_maintenance_windows.py b/tests/test_maintenance_windows.py index b1d3a3f..e1ac56a 100644 --- a/tests/test_maintenance_windows.py +++ b/tests/test_maintenance_windows.py @@ -3,8 +3,8 @@ import user_variables # pylint: disable=import-error from tests import tooling_for_test from dynatrace.tenant import maintenance -from dynatrace.requests.request_handler import TenantAPIs -from dynatrace.exceptions import InvalidDateFormatException +from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.framework.exceptions import InvalidDateFormatException CLUSTER = user_variables.FULL_SET["mockserver1"] TENANT = "tenant1" diff --git a/tests/test_settings.py b/tests/test_settings.py index 4f81d8e..87c854f 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,6 +1,6 @@ """Test Cases for dynatrace.settings""" import unittest -from dynatrace import settings +from dynatrace.framework import settings class TestSettings(unittest.TestCase): diff --git a/tests/test_topology_hosts.py b/tests/test_topology_hosts.py index 7be11ec..a1bdba6 100644 --- a/tests/test_topology_hosts.py +++ b/tests/test_topology_hosts.py @@ -4,8 +4,8 @@ import unittest from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools -from dynatrace.requests.request_handler import TenantAPIs -from dynatrace.tenant.topology import hosts +from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.tenant import hosts CLUSTER = FULL_SET["mockserver1"] TENANT = "tenant1" diff --git a/tests/test_topology_process_groups.py b/tests/test_topology_process_groups.py index b21d435..ae76a0f 100644 --- a/tests/test_topology_process_groups.py +++ b/tests/test_topology_process_groups.py @@ -3,9 +3,9 @@ import unittest from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools -from dynatrace.tenant.topology.shared import EntityTypes -from dynatrace.requests.request_handler import TenantAPIs -from dynatrace.tenant.topology import process_groups +from dynatrace.tenant.shared import EntityTypes +from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.tenant import process_groups CLUSTER = FULL_SET.get('mockserver1') TENANT = 'tenant1' diff --git a/tests/test_topology_processes.py b/tests/test_topology_processes.py index 4e9aacf..fc1d841 100644 --- a/tests/test_topology_processes.py +++ b/tests/test_topology_processes.py @@ -2,10 +2,10 @@ import unittest from tests import tooling_for_test as testtools -from dynatrace import settings -from dynatrace.requests.request_handler import TenantAPIs -from dynatrace.tenant.topology.shared import EntityTypes -from dynatrace.tenant.topology import process +from dynatrace.framework import settings +from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.tenant.shared import EntityTypes +from dynatrace.tenant import process FULL_SET = settings.get_setting("FULL_SET") CLUSTER = FULL_SET.get('mockserver1') diff --git a/tests/test_topology_services.py b/tests/test_topology_services.py index f97cd04..b16c0e5 100644 --- a/tests/test_topology_services.py +++ b/tests/test_topology_services.py @@ -3,9 +3,9 @@ import unittest from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools -from dynatrace.tenant.topology.shared import EntityTypes -from dynatrace.requests.request_handler import TenantAPIs -from dynatrace.tenant.topology import services +from dynatrace.tenant.shared import EntityTypes +from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.tenant import services CLUSTER = FULL_SET.get('mockserver1') TENANT = 'tenant1' diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index b871399..10961b5 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -2,7 +2,7 @@ import json import logging import requests -from dynatrace.requests.request_handler import generate_tenant_url +from dynatrace.framework.request_handler import generate_tenant_url logging.basicConfig(filename="testing_tools.log", level=logging.DEBUG) From cb3c1a5ed176008998c2edf81f05c7a8fcc238c5 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 17:51:13 +0100 Subject: [PATCH 085/204] #PAF-52 - final changes --- dynatrace/framework/request_handler.py | 2 +- dynatrace/tenant/shared.py | 2 +- tests/special_test_settings_no_variables.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 4534316..81d8ab1 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -2,7 +2,7 @@ from enum import Enum, auto import time import requests -from dynatrace.exceptions import InvalidAPIResponseException, ManagedClusterOnlyException +from dynatrace.framework.exceptions import InvalidAPIResponseException, ManagedClusterOnlyException requests.packages.urllib3.disable_warnings() # pylint: disable=no-member diff --git a/dynatrace/tenant/shared.py b/dynatrace/tenant/shared.py index d02801f..e8c8eb3 100644 --- a/dynatrace/tenant/shared.py +++ b/dynatrace/tenant/shared.py @@ -132,7 +132,7 @@ def get_entities(cluster, tenant, entity_type, params=None): def get_entities_by_page(cluster, tenant, entity_type, params=None): - """Get all Entities of a given type, page by page. + """Get all Entities of a given type, page by page. Returns a generator, page by page. """ if not params: diff --git a/tests/special_test_settings_no_variables.py b/tests/special_test_settings_no_variables.py index e591020..87b2e52 100644 --- a/tests/special_test_settings_no_variables.py +++ b/tests/special_test_settings_no_variables.py @@ -8,7 +8,7 @@ class TestSettingsWithoutVarFile(unittest.TestCase): def test_settings_without_var_file(self): """Test should return default value when user_variables missing""" rename("user_variables.py", "user_variables.py.tmp") - from dynatrace import settings # pylint: disable=import-outside-toplevel + from dynatrace.framework import settings # pylint: disable=import-outside-toplevel timezone = settings.get_setting("DEFAULT_TIMEZONE") self.assertEqual(timezone, "UTC") rename("user_variables.py.tmp", "user_variables.py") From ea17d2cc36ea40f61426840f5e9d550fb1e3f2e2 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 9 Oct 2020 18:04:14 +0100 Subject: [PATCH 086/204] #PAF-52: fixed shared.py & entity tests --- dynatrace/tenant/shared.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dynatrace/tenant/shared.py b/dynatrace/tenant/shared.py index e8c8eb3..1d7e8b1 100644 --- a/dynatrace/tenant/shared.py +++ b/dynatrace/tenant/shared.py @@ -128,7 +128,7 @@ def get_entities(cluster, tenant, entity_type, params=None): endpoint=rh.TenantAPIs.ENTITIES, params=params ) - return response.json().get('entities') + return response.get('entities') def get_entities_by_page(cluster, tenant, entity_type, params=None): @@ -179,9 +179,9 @@ def get_entity(cluster, tenant, entity_id, params=None): ) if response.get('totalCount') == 1: - return response.json().get('entities')[0] + return response.get('entities')[0] - return response.json().get('entities') + return response.get('entities') def get_env_entity_count(cluster, tenant, entity_type, params=None): From ea8ea4060e9484b1898667d4b4e045513287fb53 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 09:48:40 +0100 Subject: [PATCH 087/204] #PAF-50: trimmed down topology operations --- dynatrace/tenant/applications.py | 88 ------- dynatrace/tenant/custom.py | 17 -- dynatrace/tenant/entities.py | 387 +++++++++++++++++++++++++++++ dynatrace/tenant/host_groups.py | 6 +- dynatrace/tenant/hosts.py | 130 ---------- dynatrace/tenant/oneagents.py | 93 +++++++ dynatrace/tenant/process.py | 23 -- dynatrace/tenant/process_groups.py | 76 ------ dynatrace/tenant/services.py | 76 ------ dynatrace/tenant/shared.py | 349 -------------------------- 10 files changed, 483 insertions(+), 762 deletions(-) delete mode 100644 dynatrace/tenant/applications.py delete mode 100644 dynatrace/tenant/custom.py create mode 100644 dynatrace/tenant/entities.py delete mode 100644 dynatrace/tenant/hosts.py create mode 100644 dynatrace/tenant/oneagents.py delete mode 100644 dynatrace/tenant/process.py delete mode 100644 dynatrace/tenant/process_groups.py delete mode 100644 dynatrace/tenant/services.py delete mode 100644 dynatrace/tenant/shared.py diff --git a/dynatrace/tenant/applications.py b/dynatrace/tenant/applications.py deleted file mode 100644 index 3243794..0000000 --- a/dynatrace/tenant/applications.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Module for application type entity operations""" - -import dynatrace.tenant.shared as entity_api -import dynatrace.framework.request_handler as rh - - -def get_applications_tenantwide(cluster, tenant): - """Get Information for all applications in a tenant""" - return entity_api.get_entities( - cluster=cluster, - tenant=tenant, - entity_type=entity_api.EntityTypes.APPLICATION - ) - - -def get_application(cluster, tenant, entity): - """Get Information for one application in a tenant""" - return entity_api.get_entity( - cluster=cluster, - tenant=tenant, - entity_id=entity - ) - - -def set_application_properties(cluster, tenant, entity, prop_json): - """Update properties of application entity""" - response = rh.make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=rh.TenantAPIs.TAGS, - params={ - 'entitySelector': f'entityId("{entity}")' - }, - method=rh.HTTP.POST, - json=prop_json - ) - - return response.json() - - -def get_application_count_tenantwide(cluster, tenant): - """Get total count for all applications in a tenant""" - params = { - "from": "now-24h" - } - - return entity_api.get_env_entity_count( - cluster=cluster, - tenant=tenant, - entity_type=entity_api.EntityTypes.APPLICATION, - params=params - ) - - -def get_application_count_clusterwide(cluster): - """Get total count for all applications in cluster""" - return entity_api.get_cluster_entity_count( - cluster=cluster, - entity_type=entity_api.EntityTypes.APPLICATION - ) - - -def get_application_count_setwide(full_set): - """Get total count of applications in cluster set""" - return entity_api.get_set_entity_count( - full_set=full_set, - entity_type=entity_api.EntityTypes.APPLICATION - ) - - -def add_application_tags(cluster, tenant, entity, tag_list): - """Add tags to application""" - if tag_list is None: - raise TypeError("tag_list cannot be None type") - tag_json = { - 'tags': tag_list - } - return set_application_properties(cluster, tenant, entity, tag_json) - - -def get_application_baseline(cluster, tenant, entity): - """Get baselines on one application for in a tenant""" - response = rh.make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/applications/{entity}/baseline" - ) - return response.json() diff --git a/dynatrace/tenant/custom.py b/dynatrace/tenant/custom.py deleted file mode 100644 index 2259850..0000000 --- a/dynatrace/tenant/custom.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Module or custom device type entity operations""" - -import dynatrace.framework.request_handler as rh - - -def set_custom_properties(cluster, tenant, entity, prop_json): - """Creates or updates properties of custom device entity""" - if not prop_json.get('customDeviceId'): - prop_json['customDeviceId'] = entity - - return rh.make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=f'{rh.TenantAPIs.ENTITIES}/custom', - method=rh.HTTP.POST, - json=prop_json - ) diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py new file mode 100644 index 0000000..1718fd3 --- /dev/null +++ b/dynatrace/tenant/entities.py @@ -0,0 +1,387 @@ +"""Module for Entities API operations""" + +from enum import Enum, auto +from dynatrace.framework import request_handler as rh + + +class EntityTypes(Enum): + """Accepted values for EntityType arguments""" + HTTP_CHECK = auto() + RELATIONAL_DATABASE_SERVICE = auto() + APPLICATION = auto() + KUBERNETES_NODE = auto() + CONTAINER_GROUP_INSTANCE = auto() + OPENSTACK_COMPUTE_NODE = auto() + QUEUE = auto() + EBS_VOLUME = auto() + OPENSTACK_PROJECT = auto() + PROCESS_GROUP = auto() + EC2_INSTANCE = auto() + GEOLOC_SITE = auto() + DEVICE_APPLICATION_METHOD_GROUP = auto() + AWS_AVAILABILITY_ZONE = auto() + SYNTHETIC_TEST_STEP = auto() + AZURE_STORAGE_ACCOUNT = auto() + AZURE_IOT_HUB = auto() + AWS_APPLICATION_LOAD_BALANCER = auto() + CLOUD_APPLICATION_NAMESPACE = auto() + BROWSER = auto() + GEOLOCATION = auto() + HTTP_CHECK_STEP = auto() + HYPERVISOR_DISK = auto() + AZURE_APP_SERVICE_PLAN = auto() + NEUTRON_SUBNET = auto() + S3BUCKET = auto() + NETWORK_INTERFACE = auto() + QUEUE_INSTANCE = auto() + APPLICATION_METHOD_GROUP = auto() + GCP_ZONE = auto() + OPENSTACK_VM = auto() + MOBILE_APPLICATION = auto() + PROCESS_GROUP_INSTANCE = auto() + HOST_GROUP = auto() + SYNTHETIC_LOCATION = auto() + SERVICE_INSTANCE = auto() + GOOGLE_COMPUTE_ENGINE = auto() + AZURE_SERVICE_BUS_TOPIC = auto() + AZURE_TENANT = auto() + CLOUD_APPLICATION = auto() + AZURE_EVENT_HUB = auto() + DEVICE_APPLICATION_METHOD = auto() + AZURE_SERVICE_BUS_NAMESPACE = auto() + VIRTUALMACHINE = auto() + ELASTIC_LOAD_BALANCER = auto() + AZURE_SUBSCRIPTION = auto() + AZURE_REDIS_CACHE = auto() + AWS_NETWORK_LOAD_BALANCER = auto() + BOSH_DEPLOYMENT = auto() + EXTERNAL_SYNTHETIC_TEST_STEP = auto() + DOCKER_CONTAINER_GROUP_INSTANCE = auto() + APPLICATION_METHOD = auto() + AZURE_CREDENTIALS = auto() + AZURE_MGMT_GROUP = auto() + SERVICE_METHOD_GROUP = auto() + AZURE_FUNCTION_APP = auto() + AZURE_SQL_SERVER = auto() + AZURE_SQL_DATABASE = auto() + AZURE_VM = auto() + OPENSTACK_AVAILABILITY_ZONE = auto() + SWIFT_CONTAINER = auto() + CLOUD_APPLICATION_INSTANCE = auto() + SERVICE = auto() + VMWARE_DATACENTER = auto() + AZURE_EVENT_HUB_NAMESPACE = auto() + VCENTER = auto() + AZURE_SERVICE_BUS_QUEUE = auto() + SERVICE_METHOD = auto() + OS = auto() + CONTAINER_GROUP = auto() + AWS_CREDENTIALS = auto() + AZURE_SQL_ELASTIC_POOL = auto() + DATASTORE = auto() + HYPERVISOR_CLUSTER = auto() + SYNTHETIC_TEST = auto() + EXTERNAL_SYNTHETIC_TEST = auto() + AUTO_SCALING_GROUP = auto() + CUSTOM_APPLICATION = auto() + AZURE_API_MANAGEMENT_SERVICE = auto() + DISK = auto() + HYPERVISOR = auto() + CUSTOM_DEVICE = auto() + AZURE_REGION = auto() + CINDER_VOLUME = auto() + DOCKER_CONTAINER_GROUP = auto() + KUBERNETES_CLUSTER = auto() + AZURE_WEB_APP = auto() + HOST = auto() + AZURE_LOAD_BALANCER = auto() + OPENSTACK_REGION = auto() + AWS_LAMBDA_FUNCTION = auto() + AZURE_APPLICATION_GATEWAY = auto() + AZURE_VM_SCALE_SET = auto() + AZURE_COSMOS_DB = auto() + DYNAMO_DB_TABLE = auto() + CUSTOM_DEVICE_GROUP = auto() + + def __str__(self): + return str(self.name) + + def __repr__(self): + return str(self.name) + + +def get_entities(cluster, tenant, entity_type, **kwargs): + """Get all Entities of specified type.\n + + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @param entity_type - use EntityTypes enum\n + @kwargs entitySelector - used to filter entities\n + @kwargs from - timeframe start\n + @kwargs to - timeframe end\n + @kwargs fields - entity detail fields\n\n + @return - List of all entities matching the selection. + """ + # If entitySelector already present, don't overwrite + if 'entitySelector' in kwargs: + kwargs['entitySelector'] += f',type({entity_type})' + else: + kwargs['entitySelector'] = f'type({entity_type})' + + response = rh.v2_get_results_whole( + cluster=cluster, + tenant=tenant, + item='entities', + endpoint=rh.TenantAPIs.ENTITIES, + params=kwargs + ) + return response.get('entities') + + +def get_entities_by_page(cluster, tenant, entity_type, **kwargs): + """Get all Entities of specified type, page by page.\n + + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @param entity_type - use EntityTypes enum\n + @kwargs entitySelector - used to filter entities\n + @kwargs from - timeframe start\n + @kwargs to - timeframe end\n + @kwargs fields - entity detail fields\n + @kwargs pageSize - max. number of entities returned per call.\n\n + @return - Generator object (page by page) of all entities that match. + """ + # If entitySelector already present, don't overwrite + if 'entitySelector' in kwargs: + kwargs['entitySelector'] += f',type({entity_type})' + else: + kwargs['entitySelector'] = f'type({entity_type})' + + response = rh.v2_get_results_by_page( + cluster=cluster, + tenant=tenant, + endpoint=rh.TenantAPIs.ENTITIES, + item='entities', + params=kwargs + ) + + for entity in response: + yield entity + + +def get_entity(cluster, tenant, entity_id, **kwargs): + """Get the details of an entity specified by ID. + You can use more than one ID if they're comma separated (id-1,id-2).\n + + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @param entity_id - ID of monitored Entity\n + @kwargs entitySelector - used to filter entities\n + @kwargs from - timeframe start\n + @kwargs to - timeframe end\n + @kwargs fields - entity detail fields\n + @return - One entity for one ID. List of entities otherwise. + """ + # If entitySelector already present, don't overwrite + if 'entitySelector' in kwargs: + kwargs['entitySelector'] += f',entityId({entity_id})' + else: + kwargs['entitySelector'] = f'entityId({entity_id})' + + response = rh.v2_get_results_whole( + cluster=cluster, + tenant=tenant, + endpoint=rh.TenantAPIs.ENTITIES, + item='entities', + params=kwargs + ) + + if response.get('totalCount') == 1: + return response.get('entities')[0] + + return response.get('entities') + + +def get_entity_count_tenantwide(cluster, tenant, entity_type, **kwargs): + """Get the total number of entities of a given type in the tenant.\n + + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @param entity_type - use EntityTypes enum for this\n + @kwargs entitySelector - used to filter entities\n + @kwargs from - timeframe start\n + @kwargs to - timeframe end\n\n + @return - number of entities. + """ + if 'from' not in kwargs: + kwargs['from'] = "now-24h" + # pageSize is irrelevant, so make the response size minimal + kwargs['pageSize'] = 1 + # fields are irrelevant, so make the response size minimal + kwargs['fields'] = "" + + # If entitySelector already present, don't overwrite + if 'entitySelector' in kwargs: + kwargs['entitySelector'] += f',type({entity_type})' + else: + kwargs['entitySelector'] = f'type({entity_type})' + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=rh.TenantAPIs.ENTITIES, + params=kwargs + ) + + return response.json().get('totalCount') + + +def get_entity_count_clusterwide(cluster, entity_type, **kwargs): + """Get total number of entitites of a given type in the cluster.\n + + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @param entity_type - use EntityTypes enum for this\n + @kwargs entitySelector - used to filter entities\n + @kwargs from - timeframe start\n + @kwargs to - timeframe end\n\n + @return - number of entities + """ + count = 0 + for tenant in cluster['tenant']: + count += get_entity_count_tenantwide( + cluster=cluster, + tenant=tenant, + entity_type=entity_type, + params=kwargs + ) + return count + + +def get_entity_count_setwide(full_set, entity_type, **kwargs): + """Get total number of entities of a give type in the cluster set.\n + + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @param entity_type - use EntityTypes enum for this\n + @kwargs entitySelector - used to filter entities\n + @kwargs from - timeframe start\n + @kwargs to - timeframe end\n\n + @return - number of entities + """ + count = 0 + for cluster in full_set.values(): + count += get_entity_count_clusterwide( + cluster=cluster, + entity_type=entity_type, + params=kwargs + ) + return count + + +def add_tags(cluster, tenant, tag_list, **kwargs): + """Add tags to entities. + Must specify at least an Entity Type or ID in entitySelector.\n + + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @param tag_list - list of tags as dictionaries with "key" and + optionally "value" attributes\n + @kwargs entitySelector - must specify at least either type or entityId. + use EntityTypes enum for type.\n\n + @throws TypeError - if tag_list is empty or not a list\n + @throws ValueError - if neither entity_type nor entity_id are specified + """ + # Sanity checking, error handling + if not tag_list: + raise TypeError("No tags provided") + if not isinstance(tag_list, list): + raise TypeError("tags_list is not a list") + if 'type' not in kwargs['entitySelector'] \ + and 'entityId' not in kwargs['entitySelector']: + raise ValueError("entitySelector must have at least type or entityId") + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.POST, + endpoint=rh.TenantAPIs.TAGS, + params=kwargs, + json=dict(tags=tag_list) + ) + + return response + + +def delete_tag(cluster, tenant, tag_key, tag_value=None, **kwargs): + """Delete a tag from entities. + Must specify at least an Entity Type or ID in entitySelector.\n + + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @param tag_key - the key of the tag(s) to be deleted\n + @param tag_value - the value for the tag key to be deleted. + Use "all" to delete all values for the key.\n + @kwargs entitySelector - must specify at least either type or entityId. + use EntityTypes enum for type.\n\n + @throws TypeError - if tag_key is empty or missing\n + @throws ValueError - if neither entity_type nor entity_id are specified + """ + # Sanity checking, error handling + if not tag_key: + raise TypeError("No tag key provided") + if 'type' not in kwargs['entitySelector'] \ + and 'entityId' not in kwargs['entitySelector']: + raise ValueError("entitySelector must have at least type or entityId") + + # Set params for tag key & value + kwargs['key'] = tag_key + if tag_value == "all": + kwargs['deleteAllWithKey'] = True + elif tag_value: + kwargs['value'] = tag_value + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.DELETE, + endpoint=rh.TenantAPIs.TAGS, + params=kwargs + ) + return response + + +def custom_device(cluster, tenant, json_data): + """Creates or updates a custom device based on given JSON data.\n + + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @param json_data - device properties in JSON format. Valid properties are:\n + ---------- str: customDeviceId (mandatory)\n + ---------- str: displayName (mandatory)\n + ---------- str: group\n + ---------- list(str): ipAddress\n + ---------- list(int): listenPorts\n + ---------- str: type (mandatory)\n + ---------- str: faviconUrl\n + ---------- str: configUrl\n + ---------- dict(str: str): properties\n + ---------- list(str): dnsNames\n + @throws ValueError - if mandatory properties missing from JSON data + """ + # Sanity checking, error handling + if not json_data.get('customDeviceId') or not json_data.get('displayName'): + raise ValueError("JSON data is missing Device ID and/or Name.") + # json_data.type can be NoneType when device already exists + if not get_entity(cluster, tenant, json_data.get('customDeviceId')) \ + and not json_data.get('type'): + raise ValueError("type must be in JSON data when creating a device") + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.POST, + json=json_data, + endpoint=f'{rh.TenantAPIs.ENTITIES}/custom' + ) + return response diff --git a/dynatrace/tenant/host_groups.py b/dynatrace/tenant/host_groups.py index c03a5b3..2ab7d69 100644 --- a/dynatrace/tenant/host_groups.py +++ b/dynatrace/tenant/host_groups.py @@ -1,5 +1,5 @@ """Module for host group type entity operations""" -import dynatrace.tenant.shared as entity_api +from dynatrace.tenant import entities # TODO redo export function (break out to export function?) # def export_host_groups_setwide(full_set): @@ -21,10 +21,10 @@ def get_host_groups_tenantwide(cluster, tenant): Returns: Dict: List of Host Groups in the tenant """ - response = entity_api.get_entities( + response = entities.get_entities( cluster=cluster, tenant=tenant, - entity_type=entity_api.EntityTypes.HOST_GROUP, + entity_type=entities.EntityTypes.HOST_GROUP, params={ 'from': 'now-24h' } diff --git a/dynatrace/tenant/hosts.py b/dynatrace/tenant/hosts.py deleted file mode 100644 index e052186..0000000 --- a/dynatrace/tenant/hosts.py +++ /dev/null @@ -1,130 +0,0 @@ -"""Module for host type entity operations""" - -import dynatrace.tenant.shared as entity_api -import dynatrace.framework.request_handler as rh - - -def get_hosts_tenantwide(cluster, tenant, params=None): - """Get Information for all hosts in a tenant""" - return entity_api.get_entities( - cluster=cluster, - tenant=tenant, - entity_type=entity_api.EntityTypes.HOST, - params=params - ) - - -def get_host(cluster, tenant, entity, params=None): - """Get Information for one host in a tenant""" - return entity_api.get_entity( - cluster=cluster, - tenant=tenant, - entity_id=entity, - params=params - ) - - -def set_host_properties(cluster, tenant, entity, prop_json): - """Update properties of host entity""" - response = rh.make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=rh.TenantAPIs.TAGS, - params={ - 'entitySelector': f'entityId("{entity}")' - }, - method=rh.HTTP.POST, - json=prop_json - ) - - return response.json() - - -def get_host_count_tenantwide(cluster, tenant, params=None): - """Get total count for all hosts in a tenant""" - return entity_api.get_env_entity_count( - cluster=cluster, - tenant=tenant, - entity_type=entity_api.EntityTypes.HOST, - params=params - ) - - -def get_host_count_clusterwide(cluster, params=None): - """Get total count for all hosts in cluster""" - return entity_api.get_cluster_entity_count( - cluster=cluster, - entity_type=entity_api.EntityTypes.HOST, - params=params - ) - - -def get_host_count_setwide(full_set, params=None): - """Get total count of hosts in cluster set""" - return entity_api.get_set_entity_count( - full_set=full_set, - entity_type=entity_api.EntityTypes.HOST, - params=params - ) - - -def add_host_tags(cluster, tenant, entity, tag_list): - """Add tags to host""" - return entity_api.add_tags( - cluster=cluster, - tenant=tenant, - tag_list=tag_list, - entity_id=entity - ) - - -def delete_host_tag(cluster, tenant, entity, tag): - """Remove single tag from host""" - return entity_api.delete_tag( - cluster=cluster, - tenant=tenant, - tag_key=tag, - entity_id=entity - ) - - -def get_host_units_tenantwide(cluster, tenant, params=None): - """Get Host Units used by tenant - - Args: - cluster (cluster dict): Currently selected cluster - tenant (str): Tenant to operate in - params (dict, optional): Available parameters to filter by. Defaults to None. - - Returns: - float: total consumed units used in tenant - """ - consumed_host_units = 0 - host_list = rh.make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=f'{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts', - params=params - ).json() - for host in host_list: - consumed_host_units += host['consumedHostUnits'] - return consumed_host_units - - -def get_oneagents_tenantwide(cluster, tenant, params=None): - oneagents = [] - next_page_key = 1 - - while next_page_key: - if next_page_key != 1: - params['nextPageKey'] = next_page_key - - response = rh.make_api_call(cluster=cluster, - endpoint=rh.TenantAPIs.ONEAGENTS, - tenant=tenant, - params=params) - - oneagents.extend(response.json().get('hosts')) - next_page_key = response.json().get('nextPageKey') - - return oneagents diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py new file mode 100644 index 0000000..4fc03e9 --- /dev/null +++ b/dynatrace/tenant/oneagents.py @@ -0,0 +1,93 @@ +"""Module for OneAgent operations.""" + +import dynatrace.framework.request_handler as rh +from dynatrace.framework.request_handler import v2_get_results_whole + + +def get_host_units_tenantwide(cluster, tenant, **kwargs): + """Get Host Units used by the tenant + \n + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @kwargs - dictionary of query parameters valid with the API.\n + @returns - total number of host units consumed + """ + host_units = 0 + + host_list = rh.v2_get_results_whole( + cluster=cluster, + tenant=tenant, + item='hosts', + endpoint=rh.TenantAPIs.ONEAGENTS, + params=kwargs + ).get('hosts') + + for host in host_list: + host_units += host['hostInfo']['consumedHostUnits'] + + return host_units + + +def get_host_units_clusterwide(cluster, aggregated=True, **kwargs): + """Get Host Units used by the cluster. + \n + @param cluster - Dynatrace Cluster (from variable set)\n + @param aggregated - return results aggregated or split by tenant\n + @kwargs - dictionary of query parameters valid with the API.\n + @returns - total number of host units consumed or dict object + with tenants as keys if not aggregated. + """ + total_host_units = 0 + host_units = {} + + for tenant in cluster['tenant']: + tenant_host_units = get_host_units_tenantwide( + cluster=cluster, + tenant=tenant, + kwargs=kwargs + ) + total_host_units += tenant_host_units + host_units[tenant] = tenant_host_units + + return total_host_units if aggregated else host_units + + +def get_host_units_setwide(full_set, aggregated=True, **kwargs): + """Get Host Units used by the full set of clusters. + \n + @param full_set - Variable Set\n + @param aggregated - return results aggregated or split by cluster\n + @kwargs - dictionary of query parameters valid with the API.\n + @returns - total number of host units consumed or dict object + with clusters as keys if not aggregated. + """ + total_host_units = 0 + host_units = {} + + for cluster in full_set.values(): + cluster_host_units = get_host_units_clusterwide( + cluster=cluster, + kwargs=kwargs + ) + total_host_units += cluster_host_units + host_units[cluster] = cluster_host_units + + return total_host_units if aggregated else host_units + + +def get_oneagents_tenantwide(cluster, tenant, **kwargs): + """Get OneAgent details for all hosts in the tenant. + \n + @param cluster - Dynatrace Cluster (from variable set)\n + @param tenant - Dynatrace Tenant (from variable set)\n + @kwargs - dictcionary of query parameters valid with the API\n + + @returns - list of OneAgents + """ + return v2_get_results_whole( + cluster=cluster, + tenant=tenant, + endpoint=rh.TenantAPIs.ONEAGENTS, + item='hosts', + params=kwargs + ).get('hosts') diff --git a/dynatrace/tenant/process.py b/dynatrace/tenant/process.py deleted file mode 100644 index fbf4eeb..0000000 --- a/dynatrace/tenant/process.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Module for process type entity operations""" - -import dynatrace.tenant.shared as entity_api - - -def get_processes_tenantwide(cluster, tenant, params=None): - """Get Information for all processes in a tenant""" - return entity_api.get_entities( - cluster=cluster, - tenant=tenant, - entity_type=entity_api.EntityTypes.PROCESS_GROUP_INSTANCE, - params=params - ) - - -def get_process(cluster, tenant, entity, params=None): - """Get Information on one process for in a tenant""" - return entity_api.get_entity( - cluster=cluster, - tenant=tenant, - entity_id=entity, - params=params - ) diff --git a/dynatrace/tenant/process_groups.py b/dynatrace/tenant/process_groups.py deleted file mode 100644 index a06f2dd..0000000 --- a/dynatrace/tenant/process_groups.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Module for process group type entity operations""" - -import dynatrace.tenant.shared as entity_api -import dynatrace.framework.request_handler as rh - - -def get_process_groups_tenantwide(cluster, tenant): - """Get Information for all process groups in a tenant""" - return entity_api.get_entities( - cluster=cluster, - tenant=tenant, - entity_type=entity_api.EntityTypes.PROCESS_GROUP - ) - - -def get_process_group(cluster, tenant, entity): - """Get Information for one process group in a tenant""" - return entity_api.get_entity( - cluster=cluster, - tenant=tenant, - entity_id=entity - ) - - -def set_process_group_properties(cluster, tenant, entity, prop_json): - """Update properties of process group entity""" - response = rh.make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=rh.TenantAPIs.TAGS, - params={ - 'entitySelector': f'entityId("{entity}")' - }, - method=rh.HTTP.POST, - json=prop_json - ) - - return response.json() - - -def get_process_group_count_tenantwide(cluster, tenant, params=None): - """Get total count for all process groups in a tenant""" - return entity_api.get_env_entity_count( - cluster=cluster, - tenant=tenant, - entity_type=entity_api.EntityTypes.PROCESS_GROUP, - params=params - ) - - -def get_process_group_count_clusterwide(cluster, params=None): - """Get total count for all process groups in cluster""" - return entity_api.get_cluster_entity_count( - cluster=cluster, - entity_type=entity_api.EntityTypes.PROCESS_GROUP, - params=params - ) - - -def get_process_group_count_setwide(full_set, params=None): - """Get total count of process groups in cluster set""" - return entity_api.get_set_entity_count( - full_set=full_set, - entity_type=entity_api.EntityTypes.PROCESS_GROUP, - params=params - ) - - -def add_process_group_tags(cluster, tenant, entity, tag_list): - """Add tags to a process group""" - return entity_api.add_tags( - cluster=cluster, - tenant=tenant, - tag_list=tag_list, - entity_id=entity - ) diff --git a/dynatrace/tenant/services.py b/dynatrace/tenant/services.py deleted file mode 100644 index 783d919..0000000 --- a/dynatrace/tenant/services.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Module for service type entity operations""" - -import dynatrace.tenant.shared as entity_api -import dynatrace.framework.request_handler as rh - - -def get_services_tenantwide(cluster, tenant): - """Get Information for all services in a tenant""" - return entity_api.get_entities( - cluster=cluster, - tenant=tenant, - entity_type=entity_api.EntityTypes.SERVICE - ) - - -def get_service(cluster, tenant, entity): - """Get Information for one service in a tenant""" - return entity_api.get_entity( - cluster=cluster, - tenant=tenant, - entity_id=entity - ) - - -def set_service_properties(cluster, tenant, entity, prop_json): - """Update properties of service entity""" - response = rh.make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=rh.TenantAPIs.TAGS, - params={ - 'entitySelector': f'entityId("{entity}")' - }, - method=rh.HTTP.POST, - json=prop_json - ) - - return response.json() - - -def get_service_count_tenantwide(cluster, tenant, params=None): - """Get total count for all services in a tenant""" - return entity_api.get_env_entity_count( - cluster=cluster, - tenant=tenant, - entity_type=entity_api.EntityTypes.SERVICE, - params=params - ) - - -def get_service_count_clusterwide(cluster, params=None): - """Get total count for all services in cluster""" - return entity_api.get_cluster_entity_count( - cluster=cluster, - entity_type=entity_api.EntityTypes.SERVICE, - params=params - ) - - -def get_service_count_setwide(full_set, params=None): - """Get total count of services in cluster set""" - return entity_api.get_set_entity_count( - full_set=full_set, - entity_type=entity_api.EntityTypes.SERVICE, - params=params - ) - - -def add_service_tags(cluster, tenant, entity, tag_list): - """Add tags to a service""" - return entity_api.add_tags( - cluster=cluster, - tenant=tenant, - tag_list=tag_list, - entity_id=entity - ) diff --git a/dynatrace/tenant/shared.py b/dynatrace/tenant/shared.py deleted file mode 100644 index 1d7e8b1..0000000 --- a/dynatrace/tenant/shared.py +++ /dev/null @@ -1,349 +0,0 @@ -"""Module for core entity operations""" - -from enum import Enum, auto -from dynatrace.framework import request_handler as rh - - -class EntityTypes(Enum): - """Accepted values for EntityType arguments""" - HTTP_CHECK = auto() - RELATIONAL_DATABASE_SERVICE = auto() - APPLICATION = auto() - KUBERNETES_NODE = auto() - CONTAINER_GROUP_INSTANCE = auto() - OPENSTACK_COMPUTE_NODE = auto() - QUEUE = auto() - EBS_VOLUME = auto() - OPENSTACK_PROJECT = auto() - PROCESS_GROUP = auto() - EC2_INSTANCE = auto() - GEOLOC_SITE = auto() - DEVICE_APPLICATION_METHOD_GROUP = auto() - AWS_AVAILABILITY_ZONE = auto() - SYNTHETIC_TEST_STEP = auto() - AZURE_STORAGE_ACCOUNT = auto() - AZURE_IOT_HUB = auto() - AWS_APPLICATION_LOAD_BALANCER = auto() - CLOUD_APPLICATION_NAMESPACE = auto() - BROWSER = auto() - GEOLOCATION = auto() - HTTP_CHECK_STEP = auto() - HYPERVISOR_DISK = auto() - AZURE_APP_SERVICE_PLAN = auto() - NEUTRON_SUBNET = auto() - S3BUCKET = auto() - NETWORK_INTERFACE = auto() - QUEUE_INSTANCE = auto() - APPLICATION_METHOD_GROUP = auto() - GCP_ZONE = auto() - OPENSTACK_VM = auto() - MOBILE_APPLICATION = auto() - PROCESS_GROUP_INSTANCE = auto() - HOST_GROUP = auto() - SYNTHETIC_LOCATION = auto() - SERVICE_INSTANCE = auto() - GOOGLE_COMPUTE_ENGINE = auto() - AZURE_SERVICE_BUS_TOPIC = auto() - AZURE_TENANT = auto() - CLOUD_APPLICATION = auto() - AZURE_EVENT_HUB = auto() - DEVICE_APPLICATION_METHOD = auto() - AZURE_SERVICE_BUS_NAMESPACE = auto() - VIRTUALMACHINE = auto() - ELASTIC_LOAD_BALANCER = auto() - AZURE_SUBSCRIPTION = auto() - AZURE_REDIS_CACHE = auto() - AWS_NETWORK_LOAD_BALANCER = auto() - BOSH_DEPLOYMENT = auto() - EXTERNAL_SYNTHETIC_TEST_STEP = auto() - DOCKER_CONTAINER_GROUP_INSTANCE = auto() - APPLICATION_METHOD = auto() - AZURE_CREDENTIALS = auto() - AZURE_MGMT_GROUP = auto() - SERVICE_METHOD_GROUP = auto() - AZURE_FUNCTION_APP = auto() - AZURE_SQL_SERVER = auto() - AZURE_SQL_DATABASE = auto() - AZURE_VM = auto() - OPENSTACK_AVAILABILITY_ZONE = auto() - SWIFT_CONTAINER = auto() - CLOUD_APPLICATION_INSTANCE = auto() - SERVICE = auto() - VMWARE_DATACENTER = auto() - AZURE_EVENT_HUB_NAMESPACE = auto() - VCENTER = auto() - AZURE_SERVICE_BUS_QUEUE = auto() - SERVICE_METHOD = auto() - OS = auto() - CONTAINER_GROUP = auto() - AWS_CREDENTIALS = auto() - AZURE_SQL_ELASTIC_POOL = auto() - DATASTORE = auto() - HYPERVISOR_CLUSTER = auto() - SYNTHETIC_TEST = auto() - EXTERNAL_SYNTHETIC_TEST = auto() - AUTO_SCALING_GROUP = auto() - CUSTOM_APPLICATION = auto() - AZURE_API_MANAGEMENT_SERVICE = auto() - DISK = auto() - HYPERVISOR = auto() - CUSTOM_DEVICE = auto() - AZURE_REGION = auto() - CINDER_VOLUME = auto() - DOCKER_CONTAINER_GROUP = auto() - KUBERNETES_CLUSTER = auto() - AZURE_WEB_APP = auto() - HOST = auto() - AZURE_LOAD_BALANCER = auto() - OPENSTACK_REGION = auto() - AWS_LAMBDA_FUNCTION = auto() - AZURE_APPLICATION_GATEWAY = auto() - AZURE_VM_SCALE_SET = auto() - AZURE_COSMOS_DB = auto() - DYNAMO_DB_TABLE = auto() - CUSTOM_DEVICE_GROUP = auto() - - def __str__(self): - return str(self.name) - - def __repr__(self): - return str(self.name) - - -def get_entities(cluster, tenant, entity_type, params=None): - """Get all Entities of specified type. Use EntityTypes enum.""" - if not params: - params = {} - - # If params already contains entitySelector, don't overwrite - if params.get('entitySelector'): - params['entitySelector'] += f'type("{entity_type}")' - else: - params['entitySelector'] = f'type("{entity_type}")' - - response = rh.v2_get_results_whole( - cluster=cluster, - tenant=tenant, - item='entities', - endpoint=rh.TenantAPIs.ENTITIES, - params=params - ) - return response.get('entities') - - -def get_entities_by_page(cluster, tenant, entity_type, params=None): - """Get all Entities of a given type, page by page. - Returns a generator, page by page. - """ - if not params: - params = {} - - # If params already contains entitySelector, don't overwrite - if params.get('entitySelector'): - params['entitySelector'] += f'type("{entity_type}")' - else: - params['entitySelector'] = f'type("{entity_type}")' - - response = rh.v2_get_results_by_page( - cluster=cluster, - tenant=tenant, - endpoint=rh.TenantAPIs.ENTITIES, - item='entities', - params=params - ) - - for entity in response: - yield entity - - -def get_entity(cluster, tenant, entity_id, params=None): - """ - Get the details of an entity specified by ID. - You can use more than one ID if it's quoted and comma separated ("id-1","id-2") - """ - if not params: - params = {} - - # If params already contains entitySelector, don't overwrite - if params.get('entitySelector'): - params['entitySelector'] += f'entityId({entity_id})' - else: - params['entitySelector'] = f'entityId({entity_id})' - - response = rh.v2_get_results_whole( - cluster=cluster, - tenant=tenant, - endpoint=rh.TenantAPIs.ENTITIES, - item='entities', - params=params - ) - - if response.get('totalCount') == 1: - return response.get('entities')[0] - - return response.get('entities') - - -def get_env_entity_count(cluster, tenant, entity_type, params=None): - """ - Get total number of entities of a given type in an environment. - Use EntityType enum. - """ - if not params: - params = {} - - if 'from' not in params: - params['from'] = "now-24h" - - # If params already contains entitySelector, don't overwrite - if params.get('entitySelector'): - params['entitySelector'] += f'type("{entity_type}")' - else: - params['entitySelector'] = f'type("{entity_type}")' - - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=rh.TenantAPIs.ENTITIES, - params=params) - env_layer_count = response.json().get('totalCount') - return env_layer_count - - -def get_cluster_entity_count(cluster, entity_type, params=None): - """ - Get total number of entitites of a given type for all environments - in cluster. Use EntityType enum. - """ - if not params: - params = {} - - count = 0 - for tenant in cluster['tenant']: - count += get_env_entity_count( - cluster=cluster, - tenant=tenant, - entity_type=entity_type, - params=params - ) - return count - - -def get_set_entity_count(full_set, entity_type, params=None): - """Get total count for all clusters definied in variable file""" - if not params: - params = {} - - count = 0 - for cluster in full_set.values(): - count += get_cluster_entity_count( - cluster=cluster, - entity_type=entity_type, - params=params - ) - return count - - -def add_tags(cluster, tenant, tag_list, entity_type=None, entity_id=None, params=None): - """ - Add tags to entities. Must specify tag key. Must specify at least - an Entity Type or ID.\n - - @param cluster - Dynatrace Cluster\n - @param tenant - Dynatrace Tenant\n - @param tag_list - list of tags as dictionaries with "key" and - optionally "value" attributes\n - @param entity_type - use EntityTypes enum for this\n - @param entity_id - ID of entity. You can specify several IDs, quoted and - separated by a comma ("id-1","id-2").\n - @param params - other query string parameters compatible with this API.\n\n - @throws TypeError - if tag_list is empty or not a list\n - @throws ValueError - if neither entity_type nor entity_id are specified - """ - if not params: - params = {} - - # Sanity checking, error handling - if not tag_list: - raise TypeError("No tags provided") - if not isinstance(tag_list, list): - raise TypeError("tags_list is not a list") - if not any([entity_type, entity_id]): - raise ValueError("Must specifiy at least either entity_type or entity_id") - - # Params may already contain an entitySelector, we mustn't overwrite - if entity_type: - if params.get('entitySelector'): - params['entitySelector'] += f'type("{entity_type}")' - else: - params['entitySelector'] = f'type("{entity_type}")' - if entity_id: - if params.get('entitySelector'): - params['entitySelector'] += f'entityId({entity_id})' - else: - params['entitySelector'] = f'entityId({entity_id})' - - response = rh.make_api_call( - cluster=cluster, - tenant=tenant, - method=rh.HTTP.POST, - endpoint=rh.TenantAPIs.TAGS, - params=params, - json=dict(tags=tag_list) - ) - - return response - - -def delete_tag(cluster, tenant, tag_key, entity_type=None, entity_id=None, - tag_value=None, params=None): - """ - Delete tag from entities. Must specify at least an Entity Type or ID.\n - - @param cluster - Dynatrace Cluster\n - @param tenant - Dynatrace Tenant\n - @param tag_key - the key of the tag(s) to be deleted\n - @param tag_value - the value for the tag key to be deleted. - Use "all" to delete all values for the key.\n - @param entity_type - use EntityTypes enum for this\n - @param entity_id - ID of entity. You can specify several IDs, quoted and - separated by a comma ("id-1","id-2").\n - @param params - other query string parameters compatible with this API.\n\n - @throws TypeError - if tag_key is empty or missing\n - @throws ValueError - if neither entity_type nor entity_id are specified - """ - if not params: - params = {} - - # Sanity checking, error handling - if not tag_key: - raise TypeError("No tag key provided") - if not any([entity_type, entity_id]): - raise ValueError("Must specifiy at least either entity_type or entity_id") - - # Params may already contain an entitySelector, we mustn't overwrite - if entity_type: - if params.get('entitySelector'): - params['entitySelector'] += f'type("{entity_type}")' - else: - params['entitySelector'] = f'type("{entity_type}")' - if entity_id: - if params.get('entitySelector'): - params['entitySelector'] += f'entityId({entity_id})' - else: - params['entitySelector'] = f'entityId({entity_id})' - - # Set params for tag key & value - params['key'] = tag_key - if tag_value == "all": - params['deleteAllWithKey'] = True - elif tag_value: - params['value'] = tag_value - - response = rh.make_api_call( - cluster=cluster, - tenant=tenant, - method=rh.HTTP.DELETE, - endpoint=rh.TenantAPIs.TAGS, - params=params - ) - return response From 7974b6851f8266c0f91070e15e6542b4bf6174ed Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 10:18:04 +0100 Subject: [PATCH 088/204] #PAF-50: added handling of oneagents api in v2_get_results functions --- dynatrace/framework/request_handler.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 81d8ab1..5b6513e 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -126,7 +126,7 @@ def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method return response -def __get_v2_multipage_results(cluster, endpoint, cursor, item, tenant=None): +def __get_v2_multipage_results(cluster, tenant, endpoint, item, cursor, **kwargs): """ Private function: not intended for calling from outside of this module. Retrieves subsequent pages of multi-page API call and gathers just the @@ -140,11 +140,12 @@ def __get_v2_multipage_results(cluster, endpoint, cursor, item, tenant=None): """ results_full = [] while cursor: + kwargs['nextPageKey'] = cursor results_page = make_api_call( cluster=cluster, tenant=tenant, endpoint=endpoint, - params=dict(nextPageKey=cursor) + params=kwargs ).json() # Collect just the items being queried @@ -156,7 +157,7 @@ def __get_v2_multipage_results(cluster, endpoint, cursor, item, tenant=None): return results_full -def v2_get_results_whole(cluster, endpoint, item, tenant=None, params=None): +def v2_get_results_whole(cluster, tenant, endpoint, item, **kwargs): """ Gets a multi-paged result set and returns it whole. To be used with V2 API pagination where the nextPageKey is returned in the body of the response. @@ -174,7 +175,7 @@ def v2_get_results_whole(cluster, endpoint, item, tenant=None, params=None): cluster=cluster, tenant=tenant, endpoint=endpoint, - params=params + params=kwargs ).json() # In the case of multi-page, get the rest @@ -185,9 +186,10 @@ def v2_get_results_whole(cluster, endpoint, item, tenant=None, params=None): endpoint=endpoint, tenant=tenant, cursor=cursor, - item=item + item=item, + # OneAgents API requires query params stay the same + kwargs=kwargs if endpoint == TenantAPIs.ONEAGENTS else None )) - return response From b955eec1da4df391ef55fa41fcbd6e6ce211b656 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 10:22:48 +0100 Subject: [PATCH 089/204] #PAF-50: added handling of oneagents api in v2_get_results functions --- dynatrace/framework/request_handler.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 5b6513e..76e9164 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -246,7 +246,7 @@ def v1_get_results_by_page(cluster, endpoint, tenant=None, params=None): cursor = response.headers.get('next-page-key') -def v2_get_results_by_page(cluster, endpoint, item, tenant=None, params=None): +def v2_get_results_by_page(cluster, tenant, endpoint, item, **kwargs): """ Gets a multi-paged result set one page at a time. To be used with V2 API pagination where the nextPageKey is returned in the body of the response. @@ -259,15 +259,18 @@ def v2_get_results_by_page(cluster, endpoint, item, tenant=None, params=None): # We'll always make at least 1 call cursor = 1 while cursor: - # On subsequent calls, must omit all other params + # On subsequent calls, must omit all other params (except OneAgents API) if cursor != 1: - params = dict(nextPageKey=cursor) + if endpoint == TenantAPIs.ONEAGENTS: + kwargs['nextPageKey'] = cursor + else: + kwargs = dict(nextPageKey=cursor) response = make_api_call( cluster=cluster, endpoint=endpoint, tenant=tenant, - params=params + params=kwargs ).json() yield response From 48ca3271bb2ca2d4b07decc7bac21f109a4be0bc Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 12:21:10 +0100 Subject: [PATCH 090/204] #PAF-50: implemented **kwargs --- dynatrace/framework/request_handler.py | 2 +- dynatrace/tenant/oneagents.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 76e9164..7902785 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -188,7 +188,7 @@ def v2_get_results_whole(cluster, tenant, endpoint, item, **kwargs): cursor=cursor, item=item, # OneAgents API requires query params stay the same - kwargs=kwargs if endpoint == TenantAPIs.ONEAGENTS else None + **kwargs if endpoint == TenantAPIs.ONEAGENTS else None )) return response diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index 4fc03e9..d715cd9 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -19,7 +19,7 @@ def get_host_units_tenantwide(cluster, tenant, **kwargs): tenant=tenant, item='hosts', endpoint=rh.TenantAPIs.ONEAGENTS, - params=kwargs + **kwargs ).get('hosts') for host in host_list: @@ -44,7 +44,7 @@ def get_host_units_clusterwide(cluster, aggregated=True, **kwargs): tenant_host_units = get_host_units_tenantwide( cluster=cluster, tenant=tenant, - kwargs=kwargs + **kwargs ) total_host_units += tenant_host_units host_units[tenant] = tenant_host_units @@ -67,7 +67,7 @@ def get_host_units_setwide(full_set, aggregated=True, **kwargs): for cluster in full_set.values(): cluster_host_units = get_host_units_clusterwide( cluster=cluster, - kwargs=kwargs + **kwargs ) total_host_units += cluster_host_units host_units[cluster] = cluster_host_units @@ -89,5 +89,5 @@ def get_oneagents_tenantwide(cluster, tenant, **kwargs): tenant=tenant, endpoint=rh.TenantAPIs.ONEAGENTS, item='hosts', - params=kwargs + **kwargs ).get('hosts') From 354ff43d67ceb9618d8aceefa776cee2bc3c26b8 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 12:49:25 +0100 Subject: [PATCH 091/204] #PAF-50: host_groups functions now in entities. **kwargs used everywhere. --- dynatrace/tenant/entities.py | 70 +++++++++++++++++++++++--- dynatrace/tenant/host_groups.py | 88 --------------------------------- 2 files changed, 64 insertions(+), 94 deletions(-) delete mode 100644 dynatrace/tenant/host_groups.py diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index 1718fd3..b03f23d 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -111,7 +111,7 @@ def __repr__(self): def get_entities(cluster, tenant, entity_type, **kwargs): - """Get all Entities of specified type.\n + """Get all Entities of specified type in the tenant.\n @param cluster - Dynatrace Cluster (from variable set)\n @param tenant - Dynatrace Tenant (from variable set)\n @@ -133,11 +133,69 @@ def get_entities(cluster, tenant, entity_type, **kwargs): tenant=tenant, item='entities', endpoint=rh.TenantAPIs.ENTITIES, - params=kwargs + **kwargs ) return response.get('entities') +def get_entities_clusterwide(cluster, entity_type, aggregated=True, **kwargs): + """Get all Entities of specified type in the cluster. + \n + @param cluster - Dynatrace Cluster (from variable set)\n + @param entity_type - use EntityTypes enum\n + @param aggregated - whether results should be split by tenant\n + @kwargs entitySelector - used to filter entities\n + @kwargs from - timeframe start\n + @kwargs to - timeframe end\n + @kwargs fields - entity detail fields\n\n + @return - List of all entities matching the selection if not aggregated. + Dictionary with tenants as keys if aggregated. + """ + split_entities = {} + all_entities = [] + + for tenant in cluster['tenant']: + tenant_entities = get_entities( + cluster=cluster, + tenant=tenant, + entity_type=entity_type, + **kwargs + ) + all_entities.extend(tenant_entities) + split_entities[tenant] = tenant_entities + + return all_entities if aggregated else split_entities + + +def get_entities_setwide(full_set, entity_type, aggregated=True, **kwargs): + """Get all Entities of specified type in the full cluster set. + \n + @param full_set - Variable set (from user variables)\n + @param entity_type - use EntityTypes enum\n + @param aggregated - whether results should be split by cluster\n + @kwargs entitySelector - used to filter entities\n + @kwargs from - timeframe start\n + @kwargs to - timeframe end\n + @kwargs fields - entity detail fields\n\n + @return - List of all entities matching the selection if not aggregated. + Dictionary with clusters as keys if aggregated. + """ + split_entities = {} + all_entities = [] + + for cluster in full_set.values(): + cluster_entities = get_entities_clusterwide( + cluster=cluster, + entity_type=entity_type, + **kwargs + ) + + all_entities.extend(cluster_entities) + split_entities[cluster] = cluster_entities + + return all_entities if aggregated else split_entities + + def get_entities_by_page(cluster, tenant, entity_type, **kwargs): """Get all Entities of specified type, page by page.\n @@ -162,7 +220,7 @@ def get_entities_by_page(cluster, tenant, entity_type, **kwargs): tenant=tenant, endpoint=rh.TenantAPIs.ENTITIES, item='entities', - params=kwargs + **kwargs ) for entity in response: @@ -193,7 +251,7 @@ def get_entity(cluster, tenant, entity_id, **kwargs): tenant=tenant, endpoint=rh.TenantAPIs.ENTITIES, item='entities', - params=kwargs + **kwargs ) if response.get('totalCount') == 1: @@ -253,7 +311,7 @@ def get_entity_count_clusterwide(cluster, entity_type, **kwargs): cluster=cluster, tenant=tenant, entity_type=entity_type, - params=kwargs + **kwargs ) return count @@ -274,7 +332,7 @@ def get_entity_count_setwide(full_set, entity_type, **kwargs): count += get_entity_count_clusterwide( cluster=cluster, entity_type=entity_type, - params=kwargs + **kwargs ) return count diff --git a/dynatrace/tenant/host_groups.py b/dynatrace/tenant/host_groups.py deleted file mode 100644 index 2ab7d69..0000000 --- a/dynatrace/tenant/host_groups.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Module for host group type entity operations""" -from dynatrace.tenant import entities - -# TODO redo export function (break out to export function?) -# def export_host_groups_setwide(full_set): - -# get_host_groups_setwide(full_set) -# with open('txt/HostGroups - ' + envName + '.txt', 'w') as outFile: -# for groupName in hostGroups.values(): -# outFile.write(groupName+"\n") -# print(envName + " writing to 'HostGroups - " + envName + ".txt'") - - -def get_host_groups_tenantwide(cluster, tenant): - """Get all Host Groups in the Tenant - - Args: - cluster (Cluster Dict): Dictionary containing all Cluster info - tenant (str): String with the tenant name that is being selected - - Returns: - Dict: List of Host Groups in the tenant - """ - response = entities.get_entities( - cluster=cluster, - tenant=tenant, - entity_type=entities.EntityTypes.HOST_GROUP, - params={ - 'from': 'now-24h' - } - ) - host_groups = { - hg.get('entityId'): hg.get('displayName') - for hg in response - } - return host_groups - - -def get_host_groups_clusterwide(cluster, tenant_split=False): - """Get all Host Groups used in the Cluster - - Args: - cluster (cluster dict): Current cluster to operate on - tenant_split (bool): whether to split results by tenant or not - - Returns: - dict: Dictionary of all Host Groups used in the Cluster. - If split by tenant, keys are tenants, values are dicts of - tenant's host groups. - """ - host_groups = {} - - if tenant_split: - for tenant in cluster['tenant']: - host_groups[tenant] = get_host_groups_tenantwide(cluster, tenant) - else: - for tenant in cluster['tenant']: - host_groups.update( - get_host_groups_tenantwide(cluster, tenant) - ) - - return host_groups - - -def get_host_groups_setwide(full_set, tenant_split=False): - """Get all Host Groups used in the Cluster Set - - Args: - full_set (dict of cluster dict): Current cluster to operate on - tenant_split (bool): whether to split results by tenant or not - - Returns: - dict: Dictionary of all Host Groups used in the Cluster Set - If split by tenant, keys are tenants, values are dicts of - tenant's host groups. - """ - host_groups = {} - - if tenant_split: - for cluster in full_set.values(): - host_groups.update(get_host_groups_clusterwide( - cluster=cluster, - tenant_split=True)) - else: - for cluster in full_set.values(): - host_groups.update(get_host_groups_clusterwide(cluster)) - - return host_groups From 4a25d92facea99da7639430ae7e1bd58edc3a271 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 14:46:30 +0100 Subject: [PATCH 092/204] #PAF-50: updated test cases for entities --- .../requests/{hosts => entities}/tags.json | 0 .../requests/processes/tags.json | 6 - .../requests/services/tags.json | 6 - .../{hosts => entities}/get_all.json | 0 .../{hosts => entities}/get_single.json | 0 .../{hosts => entities}/v1_get_all.json | 0 .../host_groups/mock_get_general_1.json | 14 --- .../responses/processes/get_all_pgis.json | 16 --- .../responses/processes/get_all_pgs.json | 15 --- .../responses/processes/get_one_pg.json | 9 -- .../responses/processes/get_one_pgi.json | 9 -- .../responses/services/get_all.json | 15 --- .../responses/services/get_one.json | 9 -- ...est_topology_hosts.py => test_entities.py} | 71 ++++++------ tests/test_host_groups.py | 45 -------- tests/test_topology_process_groups.py | 107 ------------------ tests/test_topology_processes.py | 62 ---------- tests/test_topology_services.py | 107 ------------------ 18 files changed, 34 insertions(+), 457 deletions(-) rename tests/mockserver_payloads/requests/{hosts => entities}/tags.json (100%) delete mode 100644 tests/mockserver_payloads/requests/processes/tags.json delete mode 100644 tests/mockserver_payloads/requests/services/tags.json rename tests/mockserver_payloads/responses/{hosts => entities}/get_all.json (100%) rename tests/mockserver_payloads/responses/{hosts => entities}/get_single.json (100%) rename tests/mockserver_payloads/responses/{hosts => entities}/v1_get_all.json (100%) delete mode 100644 tests/mockserver_payloads/responses/host_groups/mock_get_general_1.json delete mode 100644 tests/mockserver_payloads/responses/processes/get_all_pgis.json delete mode 100644 tests/mockserver_payloads/responses/processes/get_all_pgs.json delete mode 100644 tests/mockserver_payloads/responses/processes/get_one_pg.json delete mode 100644 tests/mockserver_payloads/responses/processes/get_one_pgi.json delete mode 100644 tests/mockserver_payloads/responses/services/get_all.json delete mode 100644 tests/mockserver_payloads/responses/services/get_one.json rename tests/{test_topology_hosts.py => test_entities.py} (64%) delete mode 100644 tests/test_host_groups.py delete mode 100644 tests/test_topology_process_groups.py delete mode 100644 tests/test_topology_processes.py delete mode 100644 tests/test_topology_services.py diff --git a/tests/mockserver_payloads/requests/hosts/tags.json b/tests/mockserver_payloads/requests/entities/tags.json similarity index 100% rename from tests/mockserver_payloads/requests/hosts/tags.json rename to tests/mockserver_payloads/requests/entities/tags.json diff --git a/tests/mockserver_payloads/requests/processes/tags.json b/tests/mockserver_payloads/requests/processes/tags.json deleted file mode 100644 index dc89ff6..0000000 --- a/tests/mockserver_payloads/requests/processes/tags.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "tags": [ - "demo", - "example" - ] -} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/services/tags.json b/tests/mockserver_payloads/requests/services/tags.json deleted file mode 100644 index dc89ff6..0000000 --- a/tests/mockserver_payloads/requests/services/tags.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "tags": [ - "demo", - "example" - ] -} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/hosts/get_all.json b/tests/mockserver_payloads/responses/entities/get_all.json similarity index 100% rename from tests/mockserver_payloads/responses/hosts/get_all.json rename to tests/mockserver_payloads/responses/entities/get_all.json diff --git a/tests/mockserver_payloads/responses/hosts/get_single.json b/tests/mockserver_payloads/responses/entities/get_single.json similarity index 100% rename from tests/mockserver_payloads/responses/hosts/get_single.json rename to tests/mockserver_payloads/responses/entities/get_single.json diff --git a/tests/mockserver_payloads/responses/hosts/v1_get_all.json b/tests/mockserver_payloads/responses/entities/v1_get_all.json similarity index 100% rename from tests/mockserver_payloads/responses/hosts/v1_get_all.json rename to tests/mockserver_payloads/responses/entities/v1_get_all.json diff --git a/tests/mockserver_payloads/responses/host_groups/mock_get_general_1.json b/tests/mockserver_payloads/responses/host_groups/mock_get_general_1.json deleted file mode 100644 index 6524813..0000000 --- a/tests/mockserver_payloads/responses/host_groups/mock_get_general_1.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "totalCount": 2, - "pageSize": 50, - "entities": [ - { - "entityId": "HOST_GROUP-ABC123DEF456GHI7", - "displayName": "TEST_HOSTGROUP_1" - }, - { - "entityId": "HOST_GROUP-DEF345GHI678JKL9", - "displayName": "TEST_HOSTGROUP_2" - } - ] -} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_all_pgis.json b/tests/mockserver_payloads/responses/processes/get_all_pgis.json deleted file mode 100644 index 05964fe..0000000 --- a/tests/mockserver_payloads/responses/processes/get_all_pgis.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "totalCount": 3, - "pageSize": 50, - "entities": - [ - { - "entityId": "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" - }, - { - "entityId": "PROCESS_GROUP_INSTANCE-A6AAFEA17E6F60FD" - }, - { - "entityId": "PROCESS_GROUP_INSTANCE-F0967E6BFEE20424" - } - ] -} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_all_pgs.json b/tests/mockserver_payloads/responses/processes/get_all_pgs.json deleted file mode 100644 index 08464d2..0000000 --- a/tests/mockserver_payloads/responses/processes/get_all_pgs.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "totalCount": 3, - "pageSize": 50, - "entities": [ - { - "entityId": "PROCESS_GROUP-ABC123DEF456GHI7" - }, - { - "entityId": "PROCESS_GROUP-19DACA5E22637C33" - }, - { - "entityId": "PROCESS_GROUP-859E1549052CD876" - } - ] -} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_one_pg.json b/tests/mockserver_payloads/responses/processes/get_one_pg.json deleted file mode 100644 index 4fcd720..0000000 --- a/tests/mockserver_payloads/responses/processes/get_one_pg.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "totalCount": 1, - "pageSize": 50, - "entities": [ - { - "entityId": "PROCESS_GROUP-ABC123DEF456GHI7" - } - ] -} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_one_pgi.json b/tests/mockserver_payloads/responses/processes/get_one_pgi.json deleted file mode 100644 index 43f1e80..0000000 --- a/tests/mockserver_payloads/responses/processes/get_one_pgi.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "totalCount": 1, - "pageSize": 50, - "entities": [ - { - "entityId": "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" - } - ] -} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/services/get_all.json b/tests/mockserver_payloads/responses/services/get_all.json deleted file mode 100644 index 3c21793..0000000 --- a/tests/mockserver_payloads/responses/services/get_all.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "totalCount": 3, - "pageSize": 50, - "entities": [ - { - "entityId": "SERVICE-ABC123DEF456GHI7" - }, - { - "entityId": "SERVICE-C096CE0BA471AEFD" - }, - { - "entityId": "SERVICE-B71ADA892013D156" - } - ] -} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/services/get_one.json b/tests/mockserver_payloads/responses/services/get_one.json deleted file mode 100644 index 0acae91..0000000 --- a/tests/mockserver_payloads/responses/services/get_one.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "totalCount": 1, - "pageSize": 50, - "entities": [ - { - "entityId": "SERVICE-ABC123DEF456GHI7" - } - ] -} \ No newline at end of file diff --git a/tests/test_topology_hosts.py b/tests/test_entities.py similarity index 64% rename from tests/test_topology_hosts.py rename to tests/test_entities.py index a1bdba6..0ddcf97 100644 --- a/tests/test_topology_hosts.py +++ b/tests/test_entities.py @@ -1,25 +1,26 @@ """ -Test Suite for Topology Hosts +Test Suite for Entities API """ import unittest -from user_variables import FULL_SET # pylint: disable=import-error +from dynatrace.tenant.entities import EntityTypes +# from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools from dynatrace.framework.request_handler import TenantAPIs -from dynatrace.tenant import hosts +from dynatrace.tenant import entities +from variable_sets.radu_vars import FULL_SET # TODO: Delete line after testing CLUSTER = FULL_SET["mockserver1"] TENANT = "tenant1" URL_PATH = str(TenantAPIs.ENTITIES) -V1_URL_PATH = f'{TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts' TAG_URL_PATH = str(TenantAPIs.TAGS) -REQUEST_DIR = "tests/mockserver_payloads/requests/hosts" -RESPONSE_DIR = "tests/mockserver_payloads/responses/hosts" +REQUEST_DIR = "tests/mockserver_payloads/requests/entities" +RESPONSE_DIR = "tests/mockserver_payloads/responses/entities" -class TestGetHosts(unittest.TestCase): - """Tests cases for fetching topology hosts.""" +class TestGetEntities(unittest.TestCase): + """Tests cases for fetching entities.""" - def test_get_all_hosts(self): + def test_get_entities(self): """Test fetching all hosts""" response_file = f"{RESPONSE_DIR}/get_all.json" @@ -30,17 +31,17 @@ def test_get_all_hosts(self): url_path=URL_PATH, request_type="GET", parameters={ - 'entitySelector': 'type("HOST")' + 'entitySelector': 'type(HOST)' }, response_file=response_file ) - result = hosts.get_hosts_tenantwide(CLUSTER, TENANT) + result = entities.get_entities(CLUSTER, TENANT, EntityTypes.HOST) expected_result = testtools.expected_payload(response_file).get('entities') self.assertEqual(result, expected_result) - def test_get_single_host(self): - """Test fetching a specific host""" + def test_get_entity(self): + """Test fetching a single entity.""" host_id = "HOST-ABC123DEF456GHIJ" response_file = f"{RESPONSE_DIR}/get_single.json" @@ -56,12 +57,12 @@ def test_get_single_host(self): response_file=response_file ) - result = hosts.get_host(CLUSTER, TENANT, host_id) + result = entities.get_entity(CLUSTER, TENANT, host_id) expected_result = testtools.expected_payload(response_file).get('entities')[0] self.assertEqual(result, expected_result) - def test_get_host_count(self): - """Test getting the count of hosts in a tenant.""" + def test_get_entity_count_tenantwide(self): + """Test getting the count of entities within a tenant.""" response_file = f"{RESPONSE_DIR}/get_all.json" testtools.create_mockserver_expectation( @@ -72,34 +73,20 @@ def test_get_host_count(self): response_file=response_file, parameters={ 'from': 'now-24h', - 'entitySelector': 'type("HOST")' + 'pageSize': '1', + 'entitySelector': 'type(HOST)' } ) - result = hosts.get_host_count_tenantwide(CLUSTER, TENANT) + result = entities.get_entity_count_tenantwide(CLUSTER, TENANT, EntityTypes.HOST) self.assertEqual(result, 3) - def test_get_host_units(self): - """Tests getting the consumed host units in a tenant.""" - - response_file = f"{RESPONSE_DIR}/v1_get_all.json" - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - url_path=V1_URL_PATH, - request_type="GET", - response_file=response_file - ) - - result = hosts.get_host_units_tenantwide(CLUSTER, TENANT) - self.assertEqual(result, 4) - class TestHostTagging(unittest.TestCase): - """Test cases for testing host-level tagging.""" + """Test cases for testing entity tagging.""" def test_add_tags(self): - """Test adding two tags to a specific host.""" + """Test adding two tags to a specific entity.""" host_id = "HOST-ABC123DEF456GHIJ" request_file = f"{REQUEST_DIR}/tags.json" @@ -117,7 +104,12 @@ def test_add_tags(self): response_code=201 ) - result = hosts.add_host_tags(CLUSTER, TENANT, host_id, tags) + result = entities.add_tags( + cluster=CLUSTER, + tenant=TENANT, + tag_list=tags, + entitySelector=f'entityId({host_id})' + ) self.assertEqual(result.status_code, 201) def test_delete_tags(self): @@ -134,7 +126,12 @@ def test_delete_tags(self): response_code=204 ) - result = hosts.delete_host_tag(CLUSTER, TENANT, host_id, tag) + result = entities.delete_tag( + cluster=CLUSTER, + tenant=TENANT, + tag_key=tag, + entitySelector=f'entityId({host_id})' + ) self.assertEqual(204, result.status_code) diff --git a/tests/test_host_groups.py b/tests/test_host_groups.py deleted file mode 100644 index 8c541d6..0000000 --- a/tests/test_host_groups.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Testing dynatrace.tenant.host_groups""" -import unittest -import user_variables # pylint: disable=import-error -from tests import tooling_for_test as testtools -from dynatrace.framework.request_handler import TenantAPIs -from dynatrace.tenant.shared import EntityTypes -from dynatrace.tenant import host_groups - -CLUSTER = user_variables.FULL_SET["mockserver1"] -TENANT = "tenant1" -URL_PATH = f"{TenantAPIs.ENTITIES}" -TYPE = f"{EntityTypes.HOST_GROUP}" - - -class TestHostGroupFunctions(unittest.TestCase): - """General Tests for Host Group Functions""" - RESPONSE_DIR = "tests/mockserver_payloads/responses/host_groups/" - - def test_get_host_groups_tenantwide(self): - """Testing Retreival of all Host Groups within a single tenant""" - parameters = { - "from": "now-24h", - "entitySelector": f'type("{TYPE}")', - } - response_file = f"{self.RESPONSE_DIR}mock_get_general_1.json" - testtools.create_mockserver_expectation( - CLUSTER, - TENANT, - URL_PATH, - "GET", - parameters=parameters, - response_file=response_file - ) - command_tested = host_groups.get_host_groups_tenantwide( - CLUSTER, TENANT) - - expected_result = { - hg.get('entityId'): hg.get('displayName') - for hg in testtools.expected_payload(response_file).get('entities') - } - self.assertEqual(command_tested, expected_result) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_topology_process_groups.py b/tests/test_topology_process_groups.py deleted file mode 100644 index ae76a0f..0000000 --- a/tests/test_topology_process_groups.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Test Suite for Topology Process Groups""" - -import unittest -from user_variables import FULL_SET # pylint: disable=import-error -from tests import tooling_for_test as testtools -from dynatrace.tenant.shared import EntityTypes -from dynatrace.framework.request_handler import TenantAPIs -from dynatrace.tenant import process_groups - -CLUSTER = FULL_SET.get('mockserver1') -TENANT = 'tenant1' -URL_PATH = f"{TenantAPIs.ENTITIES}" -TAG_URL_PATH = f"{TenantAPIs.TAGS}" -ENTITY = f"{EntityTypes.PROCESS_GROUP}" -REQUEST_DIR = "tests/mockserver_payloads/requests/processes" -RESPONSE_DIR = "tests/mockserver_payloads/responses/processes" - - -class TestGetPGs(unittest.TestCase): - """Test cases for fetching topology process groups.""" - - def test_get_all_pgs(self): - """Test fetching all PGs""" - response_file = f"{RESPONSE_DIR}/get_all_pgs.json" - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - url_path=URL_PATH, - request_type="GET", - parameters={ - 'entitySelector': f'type("{ENTITY}")' - }, - response_file=response_file - ) - - result = process_groups.get_process_groups_tenantwide(CLUSTER, TENANT) - expected_result = testtools.expected_payload(response_file).get('entities') - self.assertEqual(result, expected_result) - - def test_get_single_pg(self): - """Test fetching single PG""" - response_file = f"{RESPONSE_DIR}/get_one_pg.json" - pg_id = "PROCESS_GROUP-ABC123DEF456GHI7" - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - url_path=URL_PATH, - request_type="GET", - parameters={ - 'entitySelector': f'entityId({pg_id})' - }, - response_file=response_file - ) - - result = process_groups.get_process_group(CLUSTER, TENANT, pg_id) - expected_result = testtools.expected_payload(response_file).get('entities')[0] - self.assertEqual(result, expected_result) - - def test_get_pg_count(self): - """Test getting the PG count tenantwide.""" - response_file = f"{RESPONSE_DIR}/get_all_pgs.json" - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - url_path=URL_PATH, - request_type="GET", - parameters={ - 'entitySelector': f'type("{ENTITY}")' - }, - response_file=response_file - ) - - result = process_groups.get_process_group_count_tenantwide(CLUSTER, TENANT) - self.assertEqual(result, 3) - - -class TestPGTags(unittest.TestCase): - """Test cases for PG tags""" - - def test_add_pg_tags(self): - """Test adding two tags to the PG.""" - pg_id = "PROCESS_GROUP-859E1549052CD876" - request_file = f"{REQUEST_DIR}/tags.json" - tags = ["demo", "example"] - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - request_type="POST", - url_path=TAG_URL_PATH, - request_file=request_file, - parameters={ - 'entitySelector': f'entityId({pg_id})' - }, - response_code=201 - ) - - result = process_groups.add_process_group_tags(CLUSTER, TENANT, - pg_id, tags) - self.assertEqual(result.status_code, 201) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_topology_processes.py b/tests/test_topology_processes.py deleted file mode 100644 index fc1d841..0000000 --- a/tests/test_topology_processes.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Test suite for Topology Processes""" - -import unittest -from tests import tooling_for_test as testtools -from dynatrace.framework import settings -from dynatrace.framework.request_handler import TenantAPIs -from dynatrace.tenant.shared import EntityTypes -from dynatrace.tenant import process - -FULL_SET = settings.get_setting("FULL_SET") -CLUSTER = FULL_SET.get('mockserver1') -TENANT = 'tenant1' -URL_PATH = f"{TenantAPIs.ENTITIES}" -TYPE = f"{EntityTypes.PROCESS_GROUP_INSTANCE}" -RESPONSE_DIR = "tests/mockserver_payloads/responses/processes" - - -class TestGetProcesses(unittest.TestCase): - """Test cases for fetching topology processes.""" - - def test_get_all_processes(self): - """Test getting all processes tenantwide.""" - response_file = f"{RESPONSE_DIR}/get_all_pgis.json" - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - url_path=URL_PATH, - request_type="GET", - parameters={ - 'entitySelector': f'type("{TYPE}")' - }, - response_file=response_file - ) - - result = process.get_processes_tenantwide(CLUSTER, TENANT) - expected_result = testtools.expected_payload(response_file).get('entities') - self.assertEqual(result, expected_result) - - def test_get_single_process(self): - """Tests getting one specific process.""" - response_file = f"{RESPONSE_DIR}/get_one_pgi.json" - process_id = "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - url_path=URL_PATH, - request_type="GET", - parameters={ - 'entitySelector': f'entityId({process_id})' - }, - response_file=response_file - ) - - result = process.get_process(CLUSTER, TENANT, process_id) - expected_result = testtools.expected_payload(response_file).get('entities')[0] - self.assertEqual(result, expected_result) - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_topology_services.py b/tests/test_topology_services.py deleted file mode 100644 index b16c0e5..0000000 --- a/tests/test_topology_services.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Test Suite for Topology Services""" - -import unittest -from user_variables import FULL_SET # pylint: disable=import-error -from tests import tooling_for_test as testtools -from dynatrace.tenant.shared import EntityTypes -from dynatrace.framework.request_handler import TenantAPIs -from dynatrace.tenant import services - -CLUSTER = FULL_SET.get('mockserver1') -TENANT = 'tenant1' -URL_PATH = f"{TenantAPIs.ENTITIES}" -TAG_URL_PATH = f"{TenantAPIs.TAGS}" -TYPE = f"{EntityTypes.SERVICE}" -REQUEST_DIR = "tests/mockserver_payloads/requests/services" -RESPONSE_DIR = "tests/mockserver_payloads/responses/services" - - -class TestGetServices(unittest.TestCase): - """Test cases for fetching topology services.""" - - def test_get_all_svc(self): - """Test fetching all services""" - response_file = f"{RESPONSE_DIR}/get_all.json" - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - url_path=URL_PATH, - request_type="GET", - parameters={ - 'entitySelector': f'type("{TYPE}")' - }, - response_file=response_file - ) - - result = services.get_services_tenantwide(CLUSTER, TENANT) - expected_result = testtools.expected_payload(response_file).get('entities') - self.assertEqual(result, expected_result) - - def test_get_single_svc(self): - """Test fetching single service""" - response_file = f"{RESPONSE_DIR}/get_one.json" - svc_id = "SERVICE-ABC123DEF456GHI7" - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - url_path=URL_PATH, - request_type="GET", - parameters={ - 'entitySelector': f'entityId({svc_id})' - }, - response_file=response_file - ) - - result = services.get_service(CLUSTER, TENANT, svc_id) - expected_result = testtools.expected_payload(response_file).get('entities')[0] - self.assertEqual(result, expected_result) - - def test_get_svc_count(self): - """Test getting the service count tenantwide.""" - response_file = f"{RESPONSE_DIR}/get_all.json" - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - url_path=URL_PATH, - request_type="GET", - parameters={ - 'from': 'now-24h', - 'entitySelector': f'type("{TYPE}")' - }, - response_file=response_file - ) - - result = services.get_service_count_tenantwide(CLUSTER, TENANT) - self.assertEqual(result, 3) - - -class TestServiceTags(unittest.TestCase): - """Test cases for service tags""" - - def test_add_svc_tags(self): - """Test adding two tags to the service.""" - svc_id = "SERVICE-ABC123DEF456GHI7" - request_file = f"{REQUEST_DIR}/tags.json" - tags = ["demo", "example"] - - testtools.create_mockserver_expectation( - cluster=CLUSTER, - tenant=TENANT, - request_type="POST", - url_path=TAG_URL_PATH, - request_file=request_file, - parameters={ - 'entitySelector': f'entityId({svc_id})' - }, - response_code=201 - ) - - result = services.add_service_tags(CLUSTER, TENANT, svc_id, tags) - self.assertEqual(result.status_code, 201) - - -if __name__ == '__main__': - unittest.main() From 7cef80b9d7748f59b60493510b92e276800df9ee Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 14:47:34 +0100 Subject: [PATCH 093/204] #PAF-50: changed variable set back to user_variables --- tests/test_entities.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_entities.py b/tests/test_entities.py index 0ddcf97..35ce1d9 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -3,12 +3,11 @@ """ import unittest from dynatrace.tenant.entities import EntityTypes -# from user_variables import FULL_SET # pylint: disable=import-error +from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools from dynatrace.framework.request_handler import TenantAPIs from dynatrace.tenant import entities -from variable_sets.radu_vars import FULL_SET # TODO: Delete line after testing CLUSTER = FULL_SET["mockserver1"] TENANT = "tenant1" URL_PATH = str(TenantAPIs.ENTITIES) From cc64866631cbf4afbd8a8e9afbeb3443fa2dcd51 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 15:39:53 +0100 Subject: [PATCH 094/204] #PAF-50: fixed setwide operations --- dynatrace/tenant/entities.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index b03f23d..746b9e3 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -183,9 +183,9 @@ def get_entities_setwide(full_set, entity_type, aggregated=True, **kwargs): split_entities = {} all_entities = [] - for cluster in full_set.values(): + for cluster in full_set: cluster_entities = get_entities_clusterwide( - cluster=cluster, + cluster=full_set[cluster], entity_type=entity_type, **kwargs ) @@ -328,9 +328,9 @@ def get_entity_count_setwide(full_set, entity_type, **kwargs): @return - number of entities """ count = 0 - for cluster in full_set.values(): + for cluster in full_set: count += get_entity_count_clusterwide( - cluster=cluster, + cluster=full_set[cluster], entity_type=entity_type, **kwargs ) From ddc55a674609551c81ba0b4ee1e1c2b55e362197 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 15:46:43 +0100 Subject: [PATCH 095/204] #PAF-50: get entities by page returns entities instead of full json --- dynatrace/tenant/entities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index 746b9e3..2003b49 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -223,8 +223,8 @@ def get_entities_by_page(cluster, tenant, entity_type, **kwargs): **kwargs ) - for entity in response: - yield entity + for page in response: + yield page.get('entities') def get_entity(cluster, tenant, entity_id, **kwargs): From 75a27194571f8d8d311c417228095ce19cbe0dab Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 15:51:51 +0100 Subject: [PATCH 096/204] #PAF-50: renamed get_entities_tenantwide --- dynatrace/tenant/entities.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index 2003b49..3300465 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -110,7 +110,7 @@ def __repr__(self): return str(self.name) -def get_entities(cluster, tenant, entity_type, **kwargs): +def get_entities_tenantwide(cluster, tenant, entity_type, **kwargs): """Get all Entities of specified type in the tenant.\n @param cluster - Dynatrace Cluster (from variable set)\n @@ -155,7 +155,7 @@ def get_entities_clusterwide(cluster, entity_type, aggregated=True, **kwargs): all_entities = [] for tenant in cluster['tenant']: - tenant_entities = get_entities( + tenant_entities = get_entities_tenantwide( cluster=cluster, tenant=tenant, entity_type=entity_type, From 126f0e5a1044a6667e3cf9a2012c3393064cc44a Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 15:52:16 +0100 Subject: [PATCH 097/204] #PAF-50: added testing for all entities functions --- tests/test_entities.py | 106 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 3 deletions(-) diff --git a/tests/test_entities.py b/tests/test_entities.py index 35ce1d9..4c7ebbb 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -3,7 +3,7 @@ """ import unittest from dynatrace.tenant.entities import EntityTypes -from user_variables import FULL_SET # pylint: disable=import-error +from variable_sets.radu_vars import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools from dynatrace.framework.request_handler import TenantAPIs from dynatrace.tenant import entities @@ -20,7 +20,7 @@ class TestGetEntities(unittest.TestCase): """Tests cases for fetching entities.""" def test_get_entities(self): - """Test fetching all hosts""" + """Test fetching all entities of given type tenant-wide""" response_file = f"{RESPONSE_DIR}/get_all.json" @@ -35,7 +35,47 @@ def test_get_entities(self): response_file=response_file ) - result = entities.get_entities(CLUSTER, TENANT, EntityTypes.HOST) + result = entities.get_entities_tenantwide(CLUSTER, TENANT, EntityTypes.HOST) + expected_result = testtools.expected_payload(response_file).get('entities') + self.assertEqual(result, expected_result) + + def test_get_entities_clusterwide(self): + """Test fetching all entities of given type cluster-wide""" + + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + parameters={ + 'entitySelector': 'type(HOST)' + }, + response_file=response_file + ) + + result = entities.get_entities_clusterwide(CLUSTER, EntityTypes.HOST) + expected_result = testtools.expected_payload(response_file).get('entities') + self.assertEqual(result, expected_result) + + def test_get_entities_setwide(self): + """Test fetching all entities of given type set-wide""" + + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + parameters={ + 'entitySelector': 'type(HOST)' + }, + response_file=response_file + ) + + result = entities.get_entities_setwide(FULL_SET, EntityTypes.HOST) expected_result = testtools.expected_payload(response_file).get('entities') self.assertEqual(result, expected_result) @@ -60,6 +100,26 @@ def test_get_entity(self): expected_result = testtools.expected_payload(response_file).get('entities')[0] self.assertEqual(result, expected_result) + def test_get_entities_by_page(self): + """Test fetching tenantwide entities by page""" + + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + parameters={ + 'entitySelector': 'type(HOST)' + }, + response_file=response_file + ) + + result = entities.get_entities_by_page(CLUSTER, TENANT, EntityTypes.HOST) + expected_result = testtools.expected_payload(response_file).get('entities') + self.assertEqual(next(result), expected_result) + def test_get_entity_count_tenantwide(self): """Test getting the count of entities within a tenant.""" @@ -80,6 +140,46 @@ def test_get_entity_count_tenantwide(self): result = entities.get_entity_count_tenantwide(CLUSTER, TENANT, EntityTypes.HOST) self.assertEqual(result, 3) + def test_get_entity_count_clusterwide(self): + """Test getting the count of entities within a cluster.""" + + response_file = f"{RESPONSE_DIR}/get_all.json" + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file, + parameters={ + 'from': 'now-24h', + 'pageSize': '1', + 'entitySelector': 'type(HOST)' + } + ) + + result = entities.get_entity_count_clusterwide(CLUSTER, EntityTypes.HOST) + self.assertEqual(result, 3) + + def test_get_entity_count_setwide(self): + """Test getting the count of entities within a full set.""" + + response_file = f"{RESPONSE_DIR}/get_all.json" + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file, + parameters={ + 'from': 'now-24h', + 'pageSize': '1', + 'entitySelector': 'type(HOST)' + } + ) + + result = entities.get_entity_count_setwide(FULL_SET, EntityTypes.HOST) + self.assertEqual(result, 3) + class TestHostTagging(unittest.TestCase): """Test cases for testing entity tagging.""" From b46e8242642e5df001174731d41d045620bb040e Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 16:04:10 +0100 Subject: [PATCH 098/204] #PAF-50: changed host unit functions to V1_HOSTS - more efficient --- dynatrace/tenant/oneagents.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index d715cd9..750abd1 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -14,18 +14,17 @@ def get_host_units_tenantwide(cluster, tenant, **kwargs): """ host_units = 0 - host_list = rh.v2_get_results_whole( + host_list = rh.v1_get_results_whole( cluster=cluster, tenant=tenant, - item='hosts', - endpoint=rh.TenantAPIs.ONEAGENTS, + endpoint=f'{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts', **kwargs - ).get('hosts') + ) for host in host_list: - host_units += host['hostInfo']['consumedHostUnits'] + host_units += round(host['consumedHostUnits'], ndigits=3) - return host_units + return round(host_units, ndigits=3) def get_host_units_clusterwide(cluster, aggregated=True, **kwargs): From 6d25a44dece5ab9ccb672b04f647ba705e682cf1 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 16:04:47 +0100 Subject: [PATCH 099/204] #PAF-50: changed other entity functions to use **kwargs --- dynatrace/framework/request_handler.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 7902785..ea5cd07 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -193,7 +193,7 @@ def v2_get_results_whole(cluster, tenant, endpoint, item, **kwargs): return response -def v1_get_results_whole(cluster, endpoint, tenant=None, params=None): +def v1_get_results_whole(cluster, endpoint, tenant, **kwargs): """ Gets a multi-paged result set and returns it whole. To be used with V1 API pagination where the next-page-key is returned in the response headers. @@ -201,19 +201,19 @@ def v1_get_results_whole(cluster, endpoint, tenant=None, params=None): @param cluster - Cluster dictionary from variable_set\n @param endpoint - API endpoint to call.\n @param tenant - String of tenant name used in cluster dictionary\n - @param params - dictionary of query string parameters + @kwargs - dictionary of query string parameters """ results = [] # We'll always make at least 1 call cursor = 1 while cursor: if cursor != 1: - params['nextPageKey'] = cursor + kwargs['nextPageKey'] = cursor response = make_api_call( cluster=cluster, tenant=tenant, endpoint=endpoint, - params=params + params=kwargs ) results.extend(response.json()) cursor = response.headers.get('next-page-key') @@ -221,7 +221,7 @@ def v1_get_results_whole(cluster, endpoint, tenant=None, params=None): return results -def v1_get_results_by_page(cluster, endpoint, tenant=None, params=None): +def v1_get_results_by_page(cluster, endpoint, tenant, **kwargs): """ Gets a multi-paged result set one page at a time. To be used with V1 API pagination where the next-page-key is returned in the response headers. @@ -234,12 +234,12 @@ def v1_get_results_by_page(cluster, endpoint, tenant=None, params=None): cursor = 1 while cursor: if cursor != 1: - params['nextPageKey'] = cursor + kwargs['nextPageKey'] = cursor response = make_api_call( cluster=cluster, tenant=tenant, endpoint=endpoint, - params=params + params=kwargs ) # Pause here and return this page of results yield response.json() From 1e26a9caa0cfa4b0dcd052ff19dc0f73adf01886 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 16:06:07 +0100 Subject: [PATCH 100/204] #PAF-50: removed unnecessary import --- dynatrace/tenant/oneagents.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index 750abd1..5724a35 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -1,7 +1,6 @@ """Module for OneAgent operations.""" import dynatrace.framework.request_handler as rh -from dynatrace.framework.request_handler import v2_get_results_whole def get_host_units_tenantwide(cluster, tenant, **kwargs): @@ -83,7 +82,7 @@ def get_oneagents_tenantwide(cluster, tenant, **kwargs): @returns - list of OneAgents """ - return v2_get_results_whole( + return rh.v2_get_results_whole( cluster=cluster, tenant=tenant, endpoint=rh.TenantAPIs.ONEAGENTS, From 48a5c3d264fb312ac44883d541e0fc7dfaf4d8ed Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 16:07:15 +0100 Subject: [PATCH 101/204] #PAF-50: changed import back to user_variables --- tests/test_entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_entities.py b/tests/test_entities.py index 4c7ebbb..2cc3d6f 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -3,7 +3,7 @@ """ import unittest from dynatrace.tenant.entities import EntityTypes -from variable_sets.radu_vars import FULL_SET # pylint: disable=import-error +from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools from dynatrace.framework.request_handler import TenantAPIs from dynatrace.tenant import entities From 2a921129a79e5bc60e142c068893456319740367 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 16:22:53 +0100 Subject: [PATCH 102/204] #PAF-50: changed parsing the full set in full set functions --- dynatrace/tenant/oneagents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index 5724a35..d6bb36c 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -62,9 +62,9 @@ def get_host_units_setwide(full_set, aggregated=True, **kwargs): total_host_units = 0 host_units = {} - for cluster in full_set.values(): + for cluster in full_set: cluster_host_units = get_host_units_clusterwide( - cluster=cluster, + cluster=full_set[cluster], **kwargs ) total_host_units += cluster_host_units From 5bedbd4515f4e21f75f09a206fd77c24c47f50bf Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 16:34:20 +0100 Subject: [PATCH 103/204] #PAF-50: added test cases for oneagents functions --- .../responses/oneagents/get_oneagents.json | 22 +++++ .../v1_get_all_hosts.json} | 0 tests/test_oneagents.py | 89 +++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 tests/mockserver_payloads/responses/oneagents/get_oneagents.json rename tests/mockserver_payloads/responses/{entities/v1_get_all.json => oneagents/v1_get_all_hosts.json} (100%) create mode 100644 tests/test_oneagents.py diff --git a/tests/mockserver_payloads/responses/oneagents/get_oneagents.json b/tests/mockserver_payloads/responses/oneagents/get_oneagents.json new file mode 100644 index 0000000..67f05db --- /dev/null +++ b/tests/mockserver_payloads/responses/oneagents/get_oneagents.json @@ -0,0 +1,22 @@ +{ + "hosts": [ + { + "hostInfo": { + "entityId": "HOST-ABC123DEF456GHIJ", + "consumedHostUnits": 0.25 + } + }, + { + "hostInfo": { + "entityId": "HOST-5B9CE4E4E14185FA", + "consumedHostUnits": 0.25 + } + }, + { + "hostInfo": { + "entityId": "HOST-421D60DB4A2EA929", + "consumedHostUnits": 3.5 + } + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/entities/v1_get_all.json b/tests/mockserver_payloads/responses/oneagents/v1_get_all_hosts.json similarity index 100% rename from tests/mockserver_payloads/responses/entities/v1_get_all.json rename to tests/mockserver_payloads/responses/oneagents/v1_get_all_hosts.json diff --git a/tests/test_oneagents.py b/tests/test_oneagents.py new file mode 100644 index 0000000..e2e0bd6 --- /dev/null +++ b/tests/test_oneagents.py @@ -0,0 +1,89 @@ +""" +Test Suite for OneAgents API/Operations +""" +import unittest + +from user_variables import FULL_SET # pylint: disable=import-error +from tests import tooling_for_test as testtools +from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.tenant import oneagents + +CLUSTER = FULL_SET["mockserver1"] +TENANT = "tenant1" +V1HOST_URL = f'{TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts' +ONEAGENTS_URL = f'{TenantAPIs.ONEAGENTS}' +RESPONSE_DIR = "tests/mockserver_payloads/responses/oneagents" + + +class TestHostUnits(unittest.TestCase): + """Test cases for retrieving host units""" + + def test_get_host_units_tenantwide(self): + """Test getting host units tenant-wide""" + + response_file = f'{RESPONSE_DIR}/v1_get_all_hosts.json' + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=V1HOST_URL, + request_type="GET", + response_file=response_file + ) + + result = oneagents.get_host_units_tenantwide(CLUSTER, TENANT) + self.assertEqual(result, 4) + + def test_get_host_units_clusterwide(self): + """Test getting host units cluster-wide""" + + response_file = f'{RESPONSE_DIR}/v1_get_all_hosts.json' + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=V1HOST_URL, + request_type="GET", + response_file=response_file + ) + + result = oneagents.get_host_units_clusterwide(CLUSTER) + self.assertEqual(result, 4) + + def test_get_host_units_setwide(self): + """Test getting host units set-wide""" + + response_file = f'{RESPONSE_DIR}/v1_get_all_hosts.json' + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=V1HOST_URL, + request_type="GET", + response_file=response_file + ) + + result = oneagents.get_host_units_setwide(FULL_SET) + self.assertEqual(result, 4) + + +class TestOneAgents(unittest.TestCase): + """Test cases for OneAgent operations""" + + def test_get_oneagents_tenantwide(self): + """Test getting OneAgents tenant-wide""" + + response_file = f'{RESPONSE_DIR}/get_oneagents.json' + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=ONEAGENTS_URL, + request_type="GET", + response_file=response_file + ) + + result = oneagents.get_oneagents_tenantwide(CLUSTER, TENANT) + expected_result = testtools.expected_payload(response_file).get('hosts') + + self.assertEqual(result, expected_result) From a95530afb751e03509f1234ca2a54429bc2cf262 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 16:46:06 +0100 Subject: [PATCH 104/204] #PAF-50: fixed unused argument in RH function --- dynatrace/framework/request_handler.py | 2 +- dynatrace/tenant/entities.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index ea5cd07..a4b3c74 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -273,7 +273,7 @@ def v2_get_results_by_page(cluster, tenant, endpoint, item, **kwargs): params=kwargs ).json() - yield response + yield response.get(item) cursor = response.get('nextPageKey') diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index 3300465..2acfa27 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -223,8 +223,8 @@ def get_entities_by_page(cluster, tenant, entity_type, **kwargs): **kwargs ) - for page in response: - yield page.get('entities') + for entities in response: + yield entities def get_entity(cluster, tenant, entity_id, **kwargs): From 1bcffbbd05224eb981547bac96d03b519f10cc08 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 10 Oct 2020 16:49:03 +0100 Subject: [PATCH 105/204] #PAF-50: fixed linting on imports --- tests/test_entities.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_entities.py b/tests/test_entities.py index 2cc3d6f..af4a10a 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -2,9 +2,9 @@ Test Suite for Entities API """ import unittest -from dynatrace.tenant.entities import EntityTypes from user_variables import FULL_SET # pylint: disable=import-error from tests import tooling_for_test as testtools +from dynatrace.tenant.entities import EntityTypes from dynatrace.framework.request_handler import TenantAPIs from dynatrace.tenant import entities From 73575b8f7f5c32645c304970e1c814cd3df1045f Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 10 Oct 2020 12:42:03 -0500 Subject: [PATCH 106/204] more merge adjustments --- .circleci/config.yml | 7 +- .circleci/mockserver.py | 2 +- .coveragerc | 9 + .github/workflows/linter.yml | 18 +- .gitignore | 2 + .pylintrc | 597 +++++++++++++++++- Documentation/GETTING_STARTED.md | 0 Documentation/HOWTO.md | 0 Documentation/MODULE_GLOSSARY.md | 330 ---------- Pipfile | 2 + Pipfile.lock | 93 ++- README.md | 4 +- change_variables.py | 6 +- dynatrace/cluster/config.py | 26 + dynatrace/cluster/{ssl.py => ssl_cert.py} | 0 dynatrace/cluster/sso.py | 10 +- dynatrace/cluster/user_groups.py | 20 +- dynatrace/cluster/users.py | 8 +- dynatrace/exceptions.py | 12 +- dynatrace/settings.py | 55 ++ dynatrace/tenant/extensions.py | 1 + dynatrace/tenant/host_groups.py | 25 + dynatrace/tenant/maintenance.py | 321 ++++++++-- dynatrace/tenant/management_zones.py | 7 +- dynatrace/tenant/metrics.py | 1 + dynatrace/tenant/request_attributes.py | 2 +- dynatrace/tenant/request_naming.py | 12 +- dynatrace/tenant/timeseries.py | 23 + dynatrace/tenant/topology/__init__.py | 7 - dynatrace/tenant/topology/applications.py | 1 + dynatrace/tenant/topology/custom.py | 1 + dynatrace/tenant/topology/hosts.py | 20 +- dynatrace/tenant/topology/process_groups.py | 8 +- dynatrace/tenant/topology/shared.py | 17 +- .../requests/hosts/tags.json | 6 + .../maintenance/mock_create_daily_1.json | 2 +- .../mock_create_daily_multi_tags_and_1.json | 28 + .../mock_create_daily_multi_tags_or_1.json | 30 + .../mock_create_daily_single_tag_1.json | 2 +- .../maintenance/mock_create_monthly_1.json | 17 + .../maintenance/mock_create_once_1.json | 12 + .../maintenance/mock_create_weekly_1.json | 17 + .../requests/processes/tags.json | 6 + .../requests/services/tags.json | 6 + .../responses/hosts/get_all.json | 14 + .../responses/hosts/get_single.json | 4 + ...create_daily_1.json => mock_create_1.json} | 2 +- .../mock_create_daily_single_tag_1.json | 5 - .../responses/processes/get_all_pgis.json | 11 + .../responses/processes/get_all_pgs.json | 11 + .../responses/processes/get_one_pg.json | 3 + .../responses/processes/get_one_pgi.json | 3 + .../responses/services/get_all.json | 11 + .../responses/services/get_one.json | 3 + tests/special_test_settings_no_variables.py | 14 + tests/test_host_groups.py | 45 +- tests/test_maintenance_windows.py | 473 +++++++++++++- tests/test_settings.py | 24 + tests/test_topology_hosts.py | 127 ++++ tests/test_topology_process_groups.py | 91 +++ tests/test_topology_processes.py | 52 ++ tests/test_topology_services.py | 89 +++ tests/tooling_for_test.py | 46 +- 63 files changed, 2296 insertions(+), 505 deletions(-) create mode 100644 .coveragerc delete mode 100644 Documentation/GETTING_STARTED.md delete mode 100644 Documentation/HOWTO.md delete mode 100644 Documentation/MODULE_GLOSSARY.md rename dynatrace/cluster/{ssl.py => ssl_cert.py} (100%) create mode 100644 dynatrace/settings.py create mode 100644 tests/mockserver_payloads/requests/hosts/tags.json create mode 100644 tests/mockserver_payloads/requests/maintenance/mock_create_daily_multi_tags_and_1.json create mode 100644 tests/mockserver_payloads/requests/maintenance/mock_create_daily_multi_tags_or_1.json create mode 100644 tests/mockserver_payloads/requests/maintenance/mock_create_monthly_1.json create mode 100644 tests/mockserver_payloads/requests/maintenance/mock_create_once_1.json create mode 100644 tests/mockserver_payloads/requests/maintenance/mock_create_weekly_1.json create mode 100644 tests/mockserver_payloads/requests/processes/tags.json create mode 100644 tests/mockserver_payloads/requests/services/tags.json create mode 100644 tests/mockserver_payloads/responses/hosts/get_all.json create mode 100644 tests/mockserver_payloads/responses/hosts/get_single.json rename tests/mockserver_payloads/responses/maintenance/{mock_create_daily_1.json => mock_create_1.json} (75%) delete mode 100644 tests/mockserver_payloads/responses/maintenance/mock_create_daily_single_tag_1.json create mode 100644 tests/mockserver_payloads/responses/processes/get_all_pgis.json create mode 100644 tests/mockserver_payloads/responses/processes/get_all_pgs.json create mode 100644 tests/mockserver_payloads/responses/processes/get_one_pg.json create mode 100644 tests/mockserver_payloads/responses/processes/get_one_pgi.json create mode 100644 tests/mockserver_payloads/responses/services/get_all.json create mode 100644 tests/mockserver_payloads/responses/services/get_one.json create mode 100644 tests/special_test_settings_no_variables.py create mode 100644 tests/test_settings.py create mode 100644 tests/test_topology_hosts.py create mode 100644 tests/test_topology_process_groups.py create mode 100644 tests/test_topology_processes.py create mode 100644 tests/test_topology_services.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 20c1262..4b229aa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -20,7 +20,12 @@ jobs: pipenv install --dev - run: command: | - pipenv run python -m unittest discover + COVERAGE_FILE=standard.coverage pipenv run coverage run -m unittest discover + COVERAGE_FILE=special1.coverage pipenv run coverage run -m unittest tests/special_test_settings_no_variables.py + pipenv run coverage combine *.coverage + pipenv run coverage html + mkdir test-results + tar -cf test-results/coverage_report.tar htmlcov/ - store_test_results: path: test-results - store_artifacts: diff --git a/.circleci/mockserver.py b/.circleci/mockserver.py index acfb7fd..96fd562 100644 --- a/.circleci/mockserver.py +++ b/.circleci/mockserver.py @@ -14,7 +14,7 @@ } } -LOG_LEVEL="INFO" +LOG_LEVEL = "INFO" # ROLE TYPE KEYS # access_env diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..93a6d95 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,9 @@ +[run] +omit = + # omit anything in a .local directory anywhere + */lib/python*/* + # omit everything in /usr + /usr/* + # omit in the test tools + tests/* + user_variables.py \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index d093e8a..f35b3d6 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -40,12 +40,28 @@ jobs: - name: Checkout Code uses: actions/checkout@v2 + - run: mkdir -p super-linter.report ################################ # Run Linter against code base # ################################ - name: Lint Code Base - uses: docker://github/super-linter:v3 + uses: github/super-linter@v3.11.0 env: VALIDATE_ALL_CODEBASE: false DEFAULT_BRANCH: master + LINTER_RULES_PATH: . + PYTHON_PYLINT_CONFIG_FILE: .pylintrc GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LOG_FILE: super-linter.log + OUTPUT_FOLDER: super-linter.report + OUTPUT_FORMAT: tap + - run: ls + ######################## + # Retreive tap reports # + ######################## + - name: Archive super-linter tap reports + uses: actions/upload-artifact@v2 + if: failure() + with: + name: linter-report + path: super-linter.report diff --git a/.gitignore b/.gitignore index 3465291..1c60234 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ templates/* **venv** # Framework log files ***.log** +***.tap** +.converge user_variables.py sandbox_script.py diff --git a/.pylintrc b/.pylintrc index 2e0f5c5..d7f9509 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1 +1,596 @@ -init-hook='from sys.path import append; from os import getcwd; append(getcwd())' \ No newline at end of file +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= +#init-hook='from sys.path import append; from os import getcwd; append(getcwd())' +init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/Documentation/GETTING_STARTED.md b/Documentation/GETTING_STARTED.md deleted file mode 100644 index e69de29..0000000 diff --git a/Documentation/HOWTO.md b/Documentation/HOWTO.md deleted file mode 100644 index e69de29..0000000 diff --git a/Documentation/MODULE_GLOSSARY.md b/Documentation/MODULE_GLOSSARY.md deleted file mode 100644 index f23447a..0000000 --- a/Documentation/MODULE_GLOSSARY.md +++ /dev/null @@ -1,330 +0,0 @@ -# Function Glossary -*Explain all functions in one file to help find the ones you may need* -
-
- -*\* Asterisk means optional argument* -## dynatrace.cluster -### cluster_config.py -- get_node_info(Cluster Dict: cluster) - - Return: JSON Dict - - Status: Ready for Use - - Description: Get cluster node information, such as node id, Hardware Info, JVM info, URIs -- get_node_config(Cluster Dict: cluster) - - Return: JSON Dict - - Status: Ready for Use - - Description: Get node configurations such as, WebUI enabled, Agent enabled, id, IP Addresses, datacenter -- set_node_config(Cluster Dict: cluster, Dict: json) - - Return: HTTP Status Code - - Status: **UNTESTED** - - Description: Set node configurations such as, WebUI enabled, Agent enabled, id, IP Addresses, datacenter - -### ssl.py -Notes: -Entity Type can only be "COLLECTOR" or "SERVER" (case sensitive). -In addition, when pushing SSL certs via the API, it is HIGHLY RECOMMENDED to allow local logins (aka Non-SSO logins) during the change. Enable SSO-only after you can manually check the cluster to verify there are no issues with SSO. - -- get_cert_details (Cluster Dict: cluster, String: entity_type, String: entity_id) - - Return: Dict - - Status: Ready for Use - - Description: Get JSON of information about the current SSL certificate in use by a specific Server Node(For sure) or Cluster ActiveGate(I think?) -- get_cert_install_status(Cluster Dict: cluster, String: entity_id) - - Return: String - - Status: **UNTESTED** - - Description: Not sure fully of the usage. I think it is for getting the status of a certificate update. -- set_cert(Cluster Dict: cluster, String: entity_type, String: entity_id, Dict: ssl_json) - - Return: Dict - - Status: Ready to Use - -### sso.py -Notes: Some of these API commands are not advertised in the Cluster Management API - -- disable_sso (Cluster Dict: cluster) - - Return: HTTP Status Code - - State: Ready for Use - - Description: Turns off SSO in the environment. Can be especially useful if SSO breaks and you need to login with a local account -- enable_sso (Cluster Dict: cluster, Boolean: disable_local*, Boolean: groups_enabled*, Boolean: is_openid*) - - Return: HTTP Status Code - - State: Ready for Use (Only tested with already linked SSO) - - Description: Enables SSO that is already configured but disabled. By default, local login is still enabled, groups are not passed via SSO and uses SAML over OpenID. -- get_sso_status (Cluster Dict: cluster) - - Return: Dict - - State: Ready for Use - - Description: Shows the current authentication settings related to SSO - -### user_groups.py -- create_app_groups (String: app_name)
- - Return: Nothing - - Status: **LIMITED** - - Description: Takes the application and creates user groups for an application set-wide.
This is currently only applying a single format:
({User_Prefix}\_{Role_Type}\_{Tenant}_{User_Suffix})
User Prefix/Suffix and Role Type are set in the variable sets - - Current Plans: - - Refactor to a function that is for a single cluster and one for the set - - Ignore any SaaS environments in the set - - Allow for user group definited to be templated, so that a user can plug in their own group format - - Add Suffix logic -- delete_app_groups (String: app_name)
- - Return: Nothing - - Status: **LIMITED** - - Description: Takes the application and removes user groups for an application set-wide.
This is currently only applying a single format:
({User_Prefix}\_{Role_Type}\_{Tenant}_{User_Suffix})
User Prefix/Suffix and Role Type are set in the variable sets - - Current Plans: - - Refactor to a function that is for a single cluster and one for the set - - Ignore any SaaS environments in the set - - Allow for user group definited to be templated, so that a user can plug in their own group format - - Add Suffix Logic -- create_app_clusterwide (Cluster Dict: cluster, String: app_name, Dict of String List: zones*) - - Return: Nothing - - Status: **INCOMPLETE** - - Description: Create all user groups, and management zones and assign the new user groups to have appropriate permissions of the new management zones created
- "zones" is an optional argument. it is a dict of string lists. The intention is that each key would be the same as the cluster tenant name, and the embedded list will contain all the customer environments/lifecycles that will need their own management zone.
Management would be created in the format "{APP}" or "{APP} - {ENV}" - - Current Plans: - - Assign appropriate permissions to the user group from the new management zones - - Creating user groups has same limitations as "[create_app_groups](#create_app_groups)" - -### users.py -Module Notes: If SaaS is passed, by default it is ignored without error or notice. For notice, pass ignore_saas=False into the functions and it will raise an exception - -- check_is_managed(Cluster Dict: cluster, Boolean: ignore_saas) - - Return: If current cluster is Managed - - Status: Ready for Use - - Description: Internal function mostly to check if the cluster is Managed. -- get_users(Cluster Dict: cluster, Boolean: ignore_saas*) - - Return: JSON of users data in cluster - - Status: Ready for Use - - Description: Get all users in cluster and details. -- add_user(Cluster Dict: cluster, Dict: user_json, Boolean: ignore_saas*) - - Return: 'OK' - - Status: Ready for Use - - Description: Add user to the cluster according to user_json Dict -- update_user(Cluster Dict: cluster, Dict: user_json, Boolean: ignore_saas*) - - Return: 'OK' - - Status: Ready for Use - - Description: Update user information for the cluster according to user_json Dict -- get_user (Cluster Dict: cluster, String: user_id, Boolean: ignore_saas*) - - Return: JSON - - Status: Ready for Use - - Description: Get information for a single user by giving the user id -- delete_user (Cluster Dict: cluster, String: user_id, Boolean: ignore_saas*) - - Return: JSON - - Status: Ready for Use - - Description: Delete single user from the Managed Cluster -- add_user_bulk (Cluster Dict: cluster, Dict: user_json, Boolean: ignore_saas*) - - Return: 'OK' - - Status: Ready for Use - - Description: Add multiple users to the cluster according to the user_json Dict - -## dynatrace.requests - -### request_hander.py -*Class Notes:
-Cluster Dict is a single cluster defined in the FULL_SET set in user_variables and follows that structure
-Endpoints should not start with a "/"
-Params are Dict of parameters that are directly passed to the API, Key should match Dynatrace param name* - -- check_response (Cluster Dict: cluster, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: Checks if the response is within the HTTP 200-299 for a successful transaction. Otherwise raises an exception with the error
-- check_managed (Cluster Dict: cluster, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: Checks if the cluster instance provided is Managed or SaaS.
- - Current Plans: - - Allow ignore by default, so exception isn't raised and the function just carries on, skipping SaaS instances. -- cluster_get (Cluster Dict: cluster, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: GET Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- cluster_post (Cluster Dict: cluster, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: POST Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- cluster_put (Cluster Dict: cluster, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: PUT Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- cluster_delete (Cluster Dict: cluster, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: DELETE Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- env_get (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: GET Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- env_post (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: POST Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- env_put (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: PUT Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- env_delete (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*) - - Return: Response Object - - Status: Ready for Use - - Description: DELETE Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- config_get (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: GET Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- config_post (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: POST Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- config_put (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Description: PUT Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Current Plans: - - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function -- config_delete (Cluster Dict: cluster, String: tenant, String: endpoint, Dict: params\*, Dict: json\*) - - Return: Response Object - - Status: Ready for Use - - Current Plans: - - Description: DELETE Request for Cluster API Operations, passing in the Cluster Dictionary, this will ensure that the cluster passed through is managed.
- - Allow specifications of what to return (e.g full response object, status code, json payload) with an option argument in function - -## dynatrace.tenant - -### host_groups.py - -- get_host_groups_tenantwide(Cluster Dict: cluster, String: tenant) - - Return: Dict - - Status: Ready for Use - - Description: Get all Host Groups in a tenant. Dict uses HostGroup ID for the Key -- get_host_groups_tenantwide(Cluster Dict: cluster, String: tenant) -- get_host_groups_clusterwide (Cluster Dict: cluster) - - Return: Dict - - Status: Ready for Use - - Description: Get all Host Groups in a Cluster. Dict uses HostGroup ID for the Key - - Current Plan: - - Add split_by_tenant optional variable to return all host groups in nested Dicts by tenant -- get_host_groups_setwide (Dict of Cluster Dict: setwide) - - Return: Dict - - Status: Ready for Use - - Description: Get all Host Groups in the full_set of Clusters. Dict uses HostGroup ID for the Key - - Current Plan: - - Add split_by_tenant optional variable to return all host groups in nested Dicts by cluster and then again by tenant - -## dynatrace.timeseries - -### timeseries.py -Note: Currently V1 only - -- get_timeseries_list (Cluster Dict: cluster, String: tenant, Dict: params\*) - - Return: Dict - - Status: Ready for Use - - Description: Get list of Timeseries Metric Available -- get_timeseries_list (Cluster Dict: cluster, String: tenant, String metric, Dict: params\*) - - Return: List/Dict (Varies based on Metric) - - Status: Ready for Use - - Description: Get individual timeseries metric -- create_custom_metric (Cluster Dict: cluster, String: tenant, String metric, Dict: json, Dict: params\*) - - Return: HTTP Status Code - - Status: **Untested** - - Description: Create custom metric -- delete_custom_metric (Cluster Dict: cluster, String: tenant, String metric) - - Return: HTTP Status Code - - Status: **Untested** - - Description: Delete custom metric using metric ID - -## dynatrace.topology - -### applications.py - -- get_applications_tenantwide (Cluster Dict: cluster, String: Tenant) - - Return: Dict - - Status: Ready for Use - - Description: Returns JSON payload for the list of applications -- get_application (Cluster Dict: cluster, String: tenant, String: entity) - - Return: Dict - - Status: Ready for Use - - Description: Returns a specific application JSON payload referred by its Entity ID -- set_application_properties(Cluster Dict: cluster, String: tenant, String: entity, Dict: prop_json) - - Return: Dict - - Status: Ready for Use - - Description: Update Properties of the Application (at the moment the API only allows adding manual tags) -- get_application_count_tenantwide (Cluster Dict: cluster, String: Tenant) - - Return: Int - - Status: Ready for Use - - Description: Get the number of Applications defined in the tenant -- get_application_count_clusterwide (Cluster Dict: cluster) - - Return: Int - - Status: Ready for Use - - Description: Get the number of Applications defined in the cluster -- get_application_count_setwide (Dict of Cluster Dict: setwide) - - Return: Int - - Status: Ready for Use - - Description: Get the number of Applications defined all the clusters/instances in the set -- get_application_baseline(cluster, tenant, entity) - - Return: Dict - - Status: **UNTESTED** - - Description: Returns baseline information about the application requested - -### custom.py - -- set_custom_properties (Cluster Dict: cluster. String tenant, String: Entity, Dict: prop_json) - - Return: Dict - - Status: **UNTESTED** - - Description: Create/Update custom device. - -### hosts.py -- get_hosts_tenantwide (Cluster Dict: cluster, String: Tenant, Dict: params\*) - - Return: Dict - - Status: Ready for Use - - Description: Returns JSON payload for the list of hosts -- get_hosts_tenantwide (Cluster Dict: cluster, String: Tenant, String: Entity, Dict: params\*) - - Return: Dict - - Status: Ready for Use - - Description: Returns JSON payload for a single host -- set_host_properties(Cluster Dict: cluster, String: tenant, String: entity, Dict: prop_json) - - Return: Dict - - Status: Ready for Use - - Description: Update Properties of the host (at the moment the API only allows adding manual tags) -- get_host_count_clusterwide (Cluster Dict: cluster) - - Return: Int - - Status: Ready for Use - - Description: Get the number of hosts defined in the cluster -- get_host_count_setwide (Dict of Cluster Dict: setwide) - - Return: Int - - Status: Ready for Use - - Description: Get the number of hosts defined all the clusters/instances in the set -- add_host_tags (Cluster Dict: cluster, String: tenant, String: entity, List: tag_list) - - Return: HTTP Status Code - - Status: Ready for Use - - Description: Add tags to host -- get_host_units_tenantwide(Cluster Dict: cluster, String: tenant, List: params\*): - - Return: Number - - Status: Ready for Use - - Description: Tally host units consumed by tenant (can be filtered down with params) - -### process_groups.py -TODO - refer to above topology explanations for now -### process.py -TODO - refer to above topology explanations for now -### services.py -TODO - refer to above topology explanations for now - -### shared.py -NOTE: This is unifying shared operations of multiple layers of the topology. It is advised that you do not use this module and use the other topology functions built on top of this. - - diff --git a/Pipfile b/Pipfile index b253a43..897d6db 100644 --- a/Pipfile +++ b/Pipfile @@ -6,6 +6,8 @@ verify_ssl = true [dev-packages] pylint = "*" autopep8 = "*" +coverage = "*" +flake8 = "*" [packages] requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 3da6634..ddbc73a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f1850de3b2311e799288920e10d9afb1837a02c65754e827532b4f40af27ab0d" + "sha256": "821caf8bcd1fbece0d9ec4853bfb6fd206066401b8eceb209b95e7f77629dc2b" }, "pipfile-spec": 6, "requires": { @@ -32,10 +32,11 @@ }, "idna": { "hashes": [ - "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb", - "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa" + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "version": "==2.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" }, "requests": { "hashes": [ @@ -47,10 +48,11 @@ }, "urllib3": { "hashes": [ - "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", - "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" ], - "version": "==1.25.9" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.10" } }, "develop": { @@ -59,21 +61,71 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], + "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "autopep8": { "hashes": [ - "sha256:60fd8c4341bab59963dafd5d2a566e94f547e660b9b396f772afe67d8481dbf0" + "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094" ], "index": "pypi", - "version": "==1.5.3" + "version": "==1.5.4" + }, + "coverage": { + "hashes": [ + "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", + "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", + "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", + "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", + "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", + "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", + "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", + "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", + "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", + "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", + "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", + "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", + "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", + "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", + "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", + "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", + "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", + "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", + "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", + "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", + "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", + "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", + "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", + "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", + "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", + "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", + "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", + "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", + "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", + "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", + "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", + "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", + "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", + "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + ], + "index": "pypi", + "version": "==5.3" + }, + "flake8": { + "hashes": [ + "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", + "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" + ], + "index": "pypi", + "version": "==3.8.3" }, "isort": { "hashes": [ - "sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1", - "sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd" + "sha256:171c5f365791073426b5ed3a156c2081a47f88c329161fd28228ff2da4c97ddb", + "sha256:ba91218eee31f1e300ecc079ef0c524cea3fc41bfbb979cbdf5fd3a889e3cfed" ], - "version": "==4.3.21" + "markers": "python_version >= '3.6' and python_version < '4.0'", + "version": "==5.5.2" }, "lazy-object-proxy": { "hashes": [ @@ -99,6 +151,7 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -113,21 +166,31 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, + "pyflakes": { + "hashes": [ + "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", + "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.2.0" + }, "pylint": { "hashes": [ - "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc", - "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c" + "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", + "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" ], "index": "pypi", - "version": "==2.5.3" + "version": "==2.6.0" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "toml": { diff --git a/README.md b/README.md index 187f8c9..da78415 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Dynatrace Python3 API Framework -Testing the "Test" Branch +![Lint Code Base](https://github.com/dynatrace-api-framework/python3-framework/workflows/Lint%20Code%20Base/badge.svg) **Python Requirements**: Requests -Python >= 3.4 (Built and tested with Python 3.8) +Python >= 3.8 **How To Use** diff --git a/change_variables.py b/change_variables.py index aba3923..e921d13 100644 --- a/change_variables.py +++ b/change_variables.py @@ -4,14 +4,14 @@ import os -def replace_set(set_file): +def replace_set(new_set_file): """Replace Variable File""" # Options are Darwin, Linux, Java and Windows. Java not supported if "Windows" in system(): os.system("copy variable_sets\\" + - str(set_file) + ".py user_variables.py") + str(new_set_file) + ".py user_variables.py") else: - os.system("cp variable_sets/" + str(set_file) + + os.system("cp variable_sets/" + str(new_set_file) + ".py user_variables.py") diff --git a/dynatrace/cluster/config.py b/dynatrace/cluster/config.py index 0d51613..f4040f5 100644 --- a/dynatrace/cluster/config.py +++ b/dynatrace/cluster/config.py @@ -1,19 +1,45 @@ +"""Cluster Config Operations for Dynatrace Managed""" import dynatrace.requests.request_handler as rh def get_node_info(cluster): + """Get Current Cluster Information + + Args: + cluster (cluster dict): Currently selected cluster + + Returns: + dict: cluster node info dictionary + """ response = rh.make_api_call(cluster=cluster, endpoint=rh.ClusterAPIs.CLUSTER) return response.json() def get_node_config(cluster): + """Get current cluster config for each node + + Args: + cluster (cluster dict): Currently selected cluster + + Returns: + dict: current cluster configuration properties + """ response = rh.make_api_call(cluster=cluster, endpoint=rh.ClusterAPIs.CONFIG) return response.json() def set_node_config(cluster, json): + """Set cluster config for each node + + Args: + cluster (cluster dict): Currently selected cluster + json (dict): Dict of all desired settings + + Returns: + int: status code of pass/failed + """ response = rh.make_api_call(cluster=cluster, endpoint=rh.ClusterAPIs.CONFIG, method=rh.HTTP.POST, diff --git a/dynatrace/cluster/ssl.py b/dynatrace/cluster/ssl_cert.py similarity index 100% rename from dynatrace/cluster/ssl.py rename to dynatrace/cluster/ssl_cert.py diff --git a/dynatrace/cluster/sso.py b/dynatrace/cluster/sso.py index df73b0a..ab881ed 100644 --- a/dynatrace/cluster/sso.py +++ b/dynatrace/cluster/sso.py @@ -1,7 +1,7 @@ """SSO Operations for Dynatrace""" import dynatrace.requests.request_handler as rh -ENDPOINT = "sso/ssoProvider" +ENDPOINT = "/api/v1.0/onpremise/sso/ssoProvider" def disable_sso(cluster): @@ -45,6 +45,14 @@ def enable_sso(cluster, disable_local=False, groups_enabled=False, is_openid=Fal def get_sso_status(cluster): + """Current Settings of SSO + + Args: + cluster (cluster dict): Currently selected cluster + + Returns: + dict: All SSO settings in their current setting + """ response = rh.make_api_call(cluster=cluster, endpoint=ENDPOINT) return response.json() diff --git a/dynatrace/cluster/user_groups.py b/dynatrace/cluster/user_groups.py index c073bac..170467e 100644 --- a/dynatrace/cluster/user_groups.py +++ b/dynatrace/cluster/user_groups.py @@ -1,6 +1,6 @@ #!/bin/python3 """Cluster Group Operations""" -import user_variables +import user_variables # pylint: disable=import-error from dynatrace.requests import request_handler as rh from dynatrace.tenant import management_zones as mzh @@ -13,6 +13,18 @@ def generate_group_name(template, user_type, tenant, app_name): + """Generate User Group according to template + + Args: + template (str): template with replacable values for variables + user_type (str): user permission type + tenant (str): tenant for user_group to match to + app_name (str): Application name + + Returns: + [type]: [description] + """ + # TODO Refactor for more replacements template = template.replace("{USER_TYPE}", user_type) template = template.replace("{TENANT}", tenant) template = template.replace("{APP_NAME}", app_name) @@ -59,6 +71,12 @@ def create_app_groups_setwide(app_name): def delete_app_groups(cluster, app_name): + """Delete Uesr Groups for Application + + Args: + cluster (cluster dict): Currently selected cluster + app_name (str): Application to remove all groups + """ role_types = user_variables.USER_GROUPS['role_types'] role_tenants = user_variables.USER_GROUPS['role_tenants'] diff --git a/dynatrace/cluster/users.py b/dynatrace/cluster/users.py index 2331503..ff1f8a3 100644 --- a/dynatrace/cluster/users.py +++ b/dynatrace/cluster/users.py @@ -51,9 +51,11 @@ def get_user(cluster, user_id, ignore_saas=True): def delete_user(cluster, user_id, ignore_saas=True): """Delete a Single User""" check_is_managed(cluster, ignore_saas) - response = rh.cluster_delete(cluster=cluster, - method=rh.HTTP.DELETE, - endpoint=f"{rh.ClusterAPIs.USERS}/{user_id}") + response = rh.make_api_call( + cluster=cluster, + endpoint=f"{rh.ClusterAPIs.USERS}/{user_id}", + method=rh.HTTP.DELETE, + ) return response.json() diff --git a/dynatrace/exceptions.py b/dynatrace/exceptions.py index 41c5abf..2b3d3b7 100644 --- a/dynatrace/exceptions.py +++ b/dynatrace/exceptions.py @@ -5,24 +5,30 @@ class InvalidAPIResponseException (Exception): + """The framework did not get an expected result from the Dynatrace API""" def __init__(self, message): + super().__init__(message) print(message, file=stderr) class InvalidDateFormatException(ValueError): + """The Date provided does not match the format needed""" def __init__(self, required_format): - self.required_format = required_format - print("Incorrect Date for following entry: %s", - required_format, file=stderr) + super().__init__() + self.message = f"Incorrect Date for following entry: {required_format}" class InvalidScopeException(ValueError): + """The Scope is incomplete or misconfigured""" def __init__(self, required_format): + super().__init__() self.required_format = required_format print("Invalid scope used. Tag required for management zone, matching rule: %s", required_format, file=stderr) class ManagedClusterOnlyException(TypeError): + """The operation is only supported on a managed cluster""" def __init__(self): + super().__init__() print("This operation is only supported on Dynatrace Managed!", file=stderr) diff --git a/dynatrace/settings.py b/dynatrace/settings.py new file mode 100644 index 0000000..deb3be0 --- /dev/null +++ b/dynatrace/settings.py @@ -0,0 +1,55 @@ +"""Module for retreiving settings safely""" +try: + import user_variables # pylint: disable=import-error + FILE_IMPORTED = True +except ImportError: + FILE_IMPORTED = False + + +DefaultSettings = { + 'LOG_LEVEL': None, + 'LOG_DIR': "logs/", + + # ROLE TYPE KEYS + # access_env + # change_settings + # install_agent + # view_logs + # view_senstive + # change_sensitive + + 'USER_GROUPS': { + "role_types": { + "access_env": "accessenv", + "change_settings": "changesettings", + "view_logs": "logviewer", + "view_sensitive": "viewsensitive" + }, + "role_tenants": [ + "nonprod", + "prod" + ] + }, + 'USER_GROUP_TEMPLATE': "prefix_{USER_TYPE}_{TENANT}_{APP_NAME}_suffix", + 'DEFAULT_TIMEZONE': "UTC", +} + + +def get_setting(attribute): + """Fetch setting from user-defined files or else default values + + Args: + attribute (str): attribute/setting to retreive + + Raises: + AttributeError: Setting not defaulted nor user-defined + + Returns: + [type]: attribute in it's correct variable type if found + """ + if FILE_IMPORTED and hasattr(user_variables, attribute): + return getattr(user_variables, attribute) + if attribute in DefaultSettings: + return DefaultSettings[attribute] + raise AttributeError( + f"{attribute} is not a valid user variable attribute!") diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index a5967e8..db43fb7 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -1,3 +1,4 @@ +"""Operations Interacting with Dynatrace Extensions""" from dynatrace.requests import request_handler as rh ENDPOINT = rh.TenantAPIs.EXTENSIONS diff --git a/dynatrace/tenant/host_groups.py b/dynatrace/tenant/host_groups.py index 495b012..968acff 100644 --- a/dynatrace/tenant/host_groups.py +++ b/dynatrace/tenant/host_groups.py @@ -12,6 +12,15 @@ def get_host_groups_tenantwide(cluster, tenant): + """Get all Host Groups in the Tenant + + Args: + cluster (Cluster Dict): Dictionary containing all Cluster info + tenant (str): String with the tenant name that is being selected + + Returns: + Dict: List of Host Groups in the tenant + """ params = { 'relativeTime': 'day', 'includeDetails': 'true' @@ -27,6 +36,14 @@ def get_host_groups_tenantwide(cluster, tenant): def get_host_groups_clusterwide(cluster): + """Get all Host Groups used in the Cluster + + Args: + cluster (cluster dict): Current cluster to operate on + + Returns: + dict: Dictionary of all Host Groups used in the Cluster + """ # TODO add split_by_tenant optional variable host_groups_custerwide = {} for tenant in cluster['tenant']: @@ -38,6 +55,14 @@ def get_host_groups_clusterwide(cluster): def get_host_groups_setwide(full_set): # TODO add split_by_tenant optional variable + """Get all Host Groups used in the Cluster Set + + Args: + full_set (dict of cluster dict): Current cluster to operate on + + Returns: + dict: Dictionary of all Host Groups used in the Cluster Set + """ host_groups_setwide = {} for cluster in full_set.values(): host_groups_setwide.update(get_host_groups_clusterwide(cluster)) diff --git a/dynatrace/tenant/maintenance.py b/dynatrace/tenant/maintenance.py index 5356161..09681bd 100644 --- a/dynatrace/tenant/maintenance.py +++ b/dynatrace/tenant/maintenance.py @@ -1,31 +1,37 @@ """Maintenance Window Operations""" import datetime import re +from enum import Enum, auto import dynatrace.requests.request_handler as rh -import user_variables as uv +import user_variables from dynatrace.exceptions import InvalidDateFormatException -from enum import Enum, auto MZ_ENDPOINT = rh.TenantAPIs.MAINTENANCE_WINDOWS + class Suppression(Enum): """ - *** NOT ACTIVE YET*** Types of suppression for create Maintenance Window JSON. Suppression is required Args: - Enum (FULL_ALERTING): Full Alerting. Entites in scope will have notes that a Maintenance Window was active - Enum (DISABLE_ALERTING): Problems detected but alerting profiles in that scope are not triggered - Enum (DISABLE_DETECTION): Problem detection completely off for the scope + Enum (DETECT_PROBLEMS_AND_ALERT): Full Detection and Alerting during Maintenance Window + Enum (DETECT_PROBLEMS_DONT_ALERT): Problems detected but alerts in scope are not triggered + Enum (DONT_DETECT_PROBLEMS): Problem detection completely off for the scope """ - FULL_ALERTING = "DETECT_PROBLEMS_AND_ALERT" - DISABLE_ALERTING = "DETECT_PROBLEMS_DONT_ALERT" - DISABLE_DETECTION = "DONT_DETECT_PROBLEMS" + DETECT_PROBLEMS_AND_ALERT = auto() + DETECT_PROBLEMS_DONT_ALERT = auto() + DONT_DETECT_PROBLEMS = auto() + + def __str__(self): + return str(self.name) -class Day(Enum): + def __repr__(self): + return str(self.name) + + +class DayOfWeek(Enum): """ - *** NOT ACTIVE YET *** Day of the Week Args: @@ -38,34 +44,249 @@ class Day(Enum): Enum (SUNDAY): SUNDAY """ - MONDAY = "MONDAY" - TUESDAY = "TUESDAY" - WEDNESDAY = "WEDNESDAY" - THURSDAY = "THURSDAY" - FRIDAY = "FRIDAY" - SATURDAY = "SATURDAY" - SUNDAY = "SUNDAY" + MONDAY = auto() + TUESDAY = auto() + WEDNESDAY = auto() + THURSDAY = auto() + FRIDAY = auto() + SATURDAY = auto() + SUNDAY = auto() + + def __str__(self): + return str(self.name) + + def __repr__(self): + return str(self.name) + + +class Context(Enum): + """Tag Contexts that are available""" + AWS = auto() + AWS_GENERIC = auto() + AZURE = auto() + CLOUD_FOUNDRY = auto() + CONTEXTLESS = auto() + ENVIRONMENT = auto() + GOOGLE_CLOUD = auto() + KUBERNETES = auto() + + def __str__(self): + return str(self.name) + + def __repr__(self): + return str(self.name) + + +class RecurrenceType(Enum): + """Recurrence of the Maintenance Window""" + DAILY = auto() + MONTHLY = auto() + ONCE = auto() + WEEKLY = auto() + + def __str__(self): + return str(self.name) + + def __repr__(self): + return str(self.name) + + +class FilterType(Enum): + """All Filter Types available for tag filters""" + APM_SECURITY_GATEWAY = auto() + APPLICATION = auto() + APPLICATION_METHOD = auto() + APPLICATION_METHOD_GROUP = auto() + APPMON_SERVER = auto() + APPMON_SYSTEM_PROFILE = auto() + AUTO_SCALING_GROUP = auto() + AUXILIARY_SYNTHETIC_TEST = auto() + AWS_APPLICATION_LOAD_BALANCER = auto() + AWS_AVAILABILITY_ZONE = auto() + AWS_CREDENTIALS = auto() + AWS_LAMBDA_FUNCTION = auto() + AWS_NETWORK_LOAD_BALANCER = auto() + AZURE_API_MANAGEMENT_SERVICE = auto() + AZURE_APPLICATION_GATEWAY = auto() + AZURE_COSMOS_DB = auto() + AZURE_CREDENTIALS = auto() + AZURE_EVENT_HUB = auto() + AZURE_EVENT_HUB_NAMESPACE = auto() + AZURE_FUNCTION_APP = auto() + AZURE_IOT_HUB = auto() + AZURE_LOAD_BALANCER = auto() + AZURE_MGMT_GROUP = auto() + AZURE_REDIS_CACHE = auto() + AZURE_REGION = auto() + AZURE_SERVICE_BUS_NAMESPACE = auto() + AZURE_SERVICE_BUS_QUEUE = auto() + AZURE_SERVICE_BUS_TOPIC = auto() + AZURE_SQL_DATABASE = auto() + AZURE_SQL_ELASTIC_POOL = auto() + AZURE_SQL_SERVER = auto() + AZURE_STORAGE_ACCOUNT = auto() + AZURE_SUBSCRIPTION = auto() + AZURE_TENANT = auto() + AZURE_VM = auto() + AZURE_VM_SCALE_SET = auto() + AZURE_WEB_APP = auto() + CF_APPLICATION = auto() + CF_FOUNDATION = auto() + CINDER_VOLUME = auto() + CLOUD_APPLICATION = auto() + CLOUD_APPLICATION_INSTANCE = auto() + CLOUD_APPLICATION_NAMESPACE = auto() + CONTAINER_GROUP = auto() + CONTAINER_GROUP_INSTANCE = auto() + CUSTOM_APPLICATION = auto() + CUSTOM_DEVICE = auto() + CUSTOM_DEVICE_GROUP = auto() + DCRUM_APPLICATION = auto() + DCRUM_SERVICE = auto() + DCRUM_SERVICE_INSTANCE = auto() + DEVICE_APPLICATION_METHOD = auto() + DISK = auto() + DOCKER_CONTAINER_GROUP = auto() + DOCKER_CONTAINER_GROUP_INSTANCE = auto() + DYNAMO_DB_TABLE = auto() + EBS_VOLUME = auto() + EC2_INSTANCE = auto() + ELASTIC_LOAD_BALANCER = auto() + ENVIRONMENT = auto() + EXTERNAL_SYNTHETIC_TEST_STEP = auto() + GCP_ZONE = auto() + GEOLOCATION = auto() + GEOLOC_SITE = auto() + GOOGLE_COMPUTE_ENGINE = auto() + HOST = auto() + HOST_GROUP = auto() + HTTP_CHECK = auto() + HTTP_CHECK_STEP = auto() + HYPERVISOR = auto() + KUBERNETES_CLUSTER = auto() + KUBERNETES_NODE = auto() + MOBILE_APPLICATION = auto() + NETWORK_INTERFACE = auto() + NEUTRON_SUBNET = auto() + OPENSTACK_PROJECT = auto() + OPENSTACK_REGION = auto() + OPENSTACK_VM = auto() + OS = auto() + PROCESS_GROUP = auto() + PROCESS_GROUP_INSTANCE = auto() + RELATIONAL_DATABASE_SERVICE = auto() + SERVICE = auto() + SERVICE_INSTANCE = auto() + SERVICE_METHOD = auto() + SERVICE_METHOD_GROUP = auto() + SWIFT_CONTAINER = auto() + SYNTHETIC_LOCATION = auto() + SYNTHETIC_TEST = auto() + SYNTHETIC_TEST_STEP = auto() + VIRTUALMACHINE = auto() + VMWARE_DATACENTER = auto() + + def __str__(self): + return str(self.name) + + def __repr__(self): + return str(self.name) + def validate_datetime(datetime_text, required_format): + """Validate input against expected DateTime format + + Args: + datetime_text (str): Time inputted + required_format (str): Expected format to validate against + + Raises: + InvalidDateFormatException: Used for incorrect format provided + """ try: datetime.datetime.strptime(datetime_text, required_format) except ValueError: - raise InvalidDateFormatException(required_format) + raise InvalidDateFormatException(required_format) from ValueError + + +def generate_tag_scope(tag, filter_type=None, management_zone_id=None): + """Generating Tag portion of scope + Args: + tag (list, dict, str): single or collection of tags + filter_type (str, optional): Type of entity to match against. Defaults to None. + management_zone_id (str, optional): Management Zone to match against. Defaults to None. + + Raises: + ValueError: Filter Type is not in acceptable values + + Returns: + dict: tag payload to be used as part of the main scope payload + """ + tag_payload = {} -def generate_scope(entities=None, filter_type=None, management_zone_id=None, tags=None, matches_any_tag=False): + if management_zone_id: + tag_payload['mzId'] = str(management_zone_id) + + if filter_type: + if filter_type in FilterType._member_names_: # pylint: disable=no-member,protected-access + tag_payload['type'] = filter_type + else: + raise ValueError( + "Invalid Filter Type! " + + "Please Refer to Enum or Dynatrace Documentation" + ) + + if isinstance(tag, list) and len(tag) > 0: + tag_payload['tags'] = tag + elif isinstance(tag, dict): + tag_payload['tags'] = [tag] + elif isinstance(tag, str): + tag_payload['tags'] = [{'context': "CONTEXTLESS", 'key': tag}] + + return tag_payload + + +def generate_scope( + entities=None, + tags=None, + filter_type=None, + management_zone_id=None, + match_any_tag=True +): + """Generate the total scope for maintenance window payload + + Args: + entities (list, optional): List of specific entities. Defaults to None. + tags (List,Dict,str, optional): List/Set/Individual Tags. Defaults to None. + filter_type (str, optional): Specific Entity Type for tag. Defaults to None. + management_zone_id ([type], optional): Specific MZ for tag. Defaults to None. + match_any_tag (bool, optional): Any vs All. Defaults to True. + + Returns: + dict: sub payload for maintenance window payload containing scope + """ if entities is None: entities = [] matches = [] - matches_payload = {} - if isinstance(filter_type, str): - matches_payload['type'] = filter_type - if management_zone_id: - matches_payload['managementZoneId'] = management_zone_id - if isinstance(tags, list): - matches_payload['tags'] = tags - matches.append(matches_payload) + if match_any_tag and isinstance(tags, list) and len(tags) > 1: + for tag in tags: + matches.append( + generate_tag_scope( + tag, + filter_type=filter_type, + management_zone_id=management_zone_id + ) + ) + else: + matches.append( + generate_tag_scope( + tags, + filter_type=filter_type, + management_zone_id=management_zone_id + ) + ) scope = { 'entities': entities, @@ -79,7 +300,7 @@ def generate_window_json(name, description, suppression, schedule, scope=None, i window_json = { "name": name, "description": description, - "suppression": suppression, + "suppression": str(suppression), "schedule": schedule } window_json['type'] = "PLANNED" if is_planned else "UNPLANNED" @@ -88,17 +309,21 @@ def generate_window_json(name, description, suppression, schedule, scope=None, i return window_json -def generate_schedule(recurrence_type, start_time, duration, range_start, range_end, day=None, zoneId=None,): +def generate_schedule( + recurrence_type, + start_time, + duration, + range_start, + range_end, + day=None, + zone_id=None, +): """Create schedule structure for maintenance window""" # This structure requires a lot of input validation - types_available = ["DAILY", "MONTHLY", "ONCE", "WEEKLY"] - days_of_week = ["FRIDAY", "MONDAY", "SATURDAY", - "SUNDAY", "THURSDAY", "TUESDAY", "WEDNESDAY"] - recurrence_type = str(recurrence_type).upper() # Check Recurrence - if recurrence_type not in types_available: + if recurrence_type not in RecurrenceType._member_names_: # pylint: disable=no-member,protected-access raise ValueError( "Invalid Recurrence Type! Allowed values are: ONCE, DAILY, WEEKLY, MONTHLY") @@ -112,8 +337,8 @@ def generate_schedule(recurrence_type, start_time, duration, range_start, range_ "end": range_end } - if zoneId is None: - schedule['zoneId'] = uv.DEFAULT_TIMEZONE + if zone_id is None: + schedule['zoneId'] = user_variables.DEFAULT_TIMEZONE if recurrence_type != "ONCE": # Check Start Time @@ -123,7 +348,7 @@ def generate_schedule(recurrence_type, start_time, duration, range_start, range_ try: int(duration) except ValueError: - ("Duration time must be an integer! Duration is length of Maintainence Window in minutes") + print("Duration time must be integer! Duration of Maintainence Window in minutes") schedule['recurrence'] = { "startTime": start_time, @@ -133,15 +358,17 @@ def generate_schedule(recurrence_type, start_time, duration, range_start, range_ # Check Weekly Day if recurrence_type == "WEEKLY": day = str(day).upper() - if day in days_of_week: + if day in DayOfWeek._member_names_: # pylint: disable=no-member,protected-access schedule['recurrence']['dayOfWeek'] = day else: raise ValueError("Invalid Weekly Day! Allowed values are " - + "SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY") + + "SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY") # Check Monthly Day if recurrence_type == "MONTHLY": - if (1 <= int(day) <= 31): + if not isinstance(day, int): + raise TypeError("Invalid type for Day of Month! Int between 1-31 required") + if 1 <= int(day) <= 31: schedule['recurrence']['dayOfMonth'] = day else: raise ValueError("Invalid Monthly Day! Allowed values are 1-31") @@ -197,19 +424,19 @@ def get_window(cluster, tenant, window_id): def parse_tag(tag_string): # Need a way to process literal colon inside a key "Parsing Tag to to Context, Key and Value" - m = re.match( + tag_match = re.match( r"(?:\[(\w+)\])?([\w\-\/`\+\.\!\@\#\$\%\^\&\*\(\)\?\[\]\{\}\,\<\>\ \:\;]+)(?:\:(\w*))?", tag_string ) tag_dictionary = {} - if m.group(1): - tag_dictionary['context'] = m.group(1) + if tag_match.group(1): + tag_dictionary['context'] = tag_match.group(1) else: tag_dictionary['context'] = "CONTEXTLESS" - tag_dictionary['key'] = m.group(2) # Key is always required + tag_dictionary['key'] = tag_match.group(2) # Key is always required - if m.group(3): - tag_dictionary['value'] = m.group(3) + if tag_match.group(3): + tag_dictionary['value'] = tag_match.group(3) return tag_dictionary diff --git a/dynatrace/tenant/management_zones.py b/dynatrace/tenant/management_zones.py index 192c587..f336940 100644 --- a/dynatrace/tenant/management_zones.py +++ b/dynatrace/tenant/management_zones.py @@ -27,8 +27,8 @@ def generate_mz_payload(application, env_zone=None): application) for rule_num in range(2, 10): - mz_payload['rules'][rule_num]['conditions'][0]['comparisonInfo']['value']['key'] = "APP: " + \ - str(application) + mz_payload['rules'][rule_num]['conditions'][0]['comparisonInfo']['value']['key'] = \ + "APP: " + str(application) if env_zone: # If environment exists, rename MZ and add environment conditions @@ -72,8 +72,7 @@ def add_management_zone(cluster, tenant, application, env_zone=None): json=mz_payload) if "id" in response.json(): return (response.json())['id'] - else: - return (response.text) + return response.text def change_management_zone(cluster, tenant, mz_id, application, env_zone=None): diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index ed78db9..2d65ed4 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -1,3 +1,4 @@ +"""Module for interacting with the Metrics API""" from dynatrace.requests import request_handler as rh ENDPOINT = rh.TenantAPIs.METRICS diff --git a/dynatrace/tenant/request_attributes.py b/dynatrace/tenant/request_attributes.py index d1340ac..bf77e56 100644 --- a/dynatrace/tenant/request_attributes.py +++ b/dynatrace/tenant/request_attributes.py @@ -9,7 +9,7 @@ def pull_to_files(cluster, tenant, ignore_disabled=True): """Pull files from an environment to local""" # API Calls needed: Pull RA, take the ID and pull the details of each RA - all_ra_call = rh.make_api_call(cluster=cluster, + all_ra_call = rh.make_api_call(cluster=cluster, tenant=tenant, endpoint=ENDPOINT) all_ra_json = all_ra_call.json() diff --git a/dynatrace/tenant/request_naming.py b/dynatrace/tenant/request_naming.py index e284524..38cb48f 100644 --- a/dynatrace/tenant/request_naming.py +++ b/dynatrace/tenant/request_naming.py @@ -38,15 +38,9 @@ def pull_to_files(cluster, tenant, ignore_disabled=True): return rules_file_list -def push_from_files(file_list, cluster, tenant): - """Push Service Naming Rules from Files""" - # TODO add safeties - for file_name in file_list: - print(file_name) - - def generate_file_list(): + """Generate File List from files in JSON directory""" file_list = os.listdir("./jsons/request_naming/") - for f in file_list: - print(str.isdigit(f)) + for file_name in file_list: + print(str.isdigit(file_name)) # print(file_list.sort(key=lambda f: filter(str.isdigit, f))) diff --git a/dynatrace/tenant/timeseries.py b/dynatrace/tenant/timeseries.py index be9e8ce..89873b0 100644 --- a/dynatrace/tenant/timeseries.py +++ b/dynatrace/tenant/timeseries.py @@ -1,3 +1,4 @@ +"""Timerseries Operations from Environment V1 API""" from dynatrace.requests import request_handler as rh ENDPOINT = "timeseries/" @@ -17,6 +18,18 @@ def get_timeseries_metric(cluster, tenant, metric, params=None): def create_custom_metric(cluster, tenant, metric, json, params=None): + """Create a custome timeseries metric + + Args: + cluster (cluster dict): Currently selected cluster + tenant (str): tenant to pull metrics from + metric (str): selected metric to store as + json (dict): json payload required to created custom metric + params (dict, optional): [description]. Defaults to None. + + Returns: + int: response status code + """ response = rh.make_api_call(cluster=cluster, tenant=tenant, endpoint=f"{ENDPOINT}{metric}", @@ -27,6 +40,16 @@ def create_custom_metric(cluster, tenant, metric, json, params=None): def delete_custom_metic(cluster, tenant, metric): + """[summary] + + Args: + cluster (cluster dict): Currently selected cluster + tenant (str): Tenant to operate in + metric (str): custom metric to be deleted + + Returns: + [type]: [description] + """ response = rh.make_api_call(cluster=cluster, tenant=tenant, method=rh.HTTP.DELETE, diff --git a/dynatrace/tenant/topology/__init__.py b/dynatrace/tenant/topology/__init__.py index 17f1f19..e69de29 100644 --- a/dynatrace/tenant/topology/__init__.py +++ b/dynatrace/tenant/topology/__init__.py @@ -1,7 +0,0 @@ -from dynatrace.tenant.topology.applications import * -from dynatrace.tenant.topology.custom import * -from dynatrace.tenant.topology.hosts import * -from dynatrace.tenant.topology.process import * -from dynatrace.tenant.topology.process_groups import * -from dynatrace.tenant.topology.services import * -from dynatrace.tenant.topology.shared import * diff --git a/dynatrace/tenant/topology/applications.py b/dynatrace/tenant/topology/applications.py index 083ee34..f6f4985 100644 --- a/dynatrace/tenant/topology/applications.py +++ b/dynatrace/tenant/topology/applications.py @@ -57,6 +57,7 @@ def get_application_count_clusterwide(cluster): def get_application_count_setwide(full_set): + """Get total count of applications in cluster set""" full_set_app_count = 0 for cluster_items in full_set.values(): full_set_app_count = full_set_app_count \ diff --git a/dynatrace/tenant/topology/custom.py b/dynatrace/tenant/topology/custom.py index 658f62f..3868379 100644 --- a/dynatrace/tenant/topology/custom.py +++ b/dynatrace/tenant/topology/custom.py @@ -1,3 +1,4 @@ +"""Module for interacting with Custom Topology Actions""" import dynatrace.tenant.topology.shared as topology_shared diff --git a/dynatrace/tenant/topology/hosts.py b/dynatrace/tenant/topology/hosts.py index 822725c..1419fb3 100644 --- a/dynatrace/tenant/topology/hosts.py +++ b/dynatrace/tenant/topology/hosts.py @@ -42,13 +42,25 @@ def delete_host_tag(cluster, tenant, entity, tag): """Remove single tag from host""" if tag is None: raise TypeError("Tag cannot be None!") - return rh.make_api_call(cluster=cluster, - tenant=tenant, - method=rh.HTTP.DELETE, - endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts/{entity}/tags/{tag}") + return rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.DELETE, + endpoint=f"{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts/{entity}/tags/{tag}" + ) def get_host_units_tenantwide(cluster, tenant, params=None): + """Get Host Units used by tenant + + Args: + cluster (cluster dict): Currently selected cluster + tenant (str): Tenant to operate in + params (dict, optional): Available parameters to filter by. Defaults to None. + + Returns: + float: total consumed units used in tenant + """ consumed_host_units = 0 host_list = get_hosts_tenantwide(cluster, tenant, params=params) for host in host_list: diff --git a/dynatrace/tenant/topology/process_groups.py b/dynatrace/tenant/topology/process_groups.py index e45d912..3c169c5 100644 --- a/dynatrace/tenant/topology/process_groups.py +++ b/dynatrace/tenant/topology/process_groups.py @@ -14,7 +14,13 @@ def get_process_group(cluster, tenant, entity): def set_process_group_properties(cluster, tenant, entity, prop_json): """Update properties of process-group entity""" - return topology_shared.set_env_layer_properties(cluster, tenant, 'process-groups', entity, prop_json) + return topology_shared.set_env_layer_properties( + cluster, + tenant, + 'process-groups', + entity, + prop_json + ) def get_process_group_count_tenantwide(cluster, tenant, params=None): diff --git a/dynatrace/tenant/topology/shared.py b/dynatrace/tenant/topology/shared.py index 32f35ef..47f9c39 100644 --- a/dynatrace/tenant/topology/shared.py +++ b/dynatrace/tenant/topology/shared.py @@ -127,6 +127,21 @@ def get_set_layer_count(full_set, layer, params=None): def add_env_layer_tags(cluster, tenant, layer, entity, tag_list): + """Add tags to entity Layer + + Args: + cluster (cluster dict): Currently selected cluster + tenant (str): Tenant to operate in + layer (str): Topology Layer to pull from + entity (str): Entity to add tags to + tag_list (list): All tags to add to entity + + Raises: + TypeError: [description] + + Returns: + [type]: [description] + """ layer_list = ['applications', 'hosts', 'custom', 'process-groups', 'services'] check_valid_layer(layer, layer_list) @@ -135,4 +150,4 @@ def add_env_layer_tags(cluster, tenant, layer, entity, tag_list): tag_json = { 'tags': tag_list } - return set_env_layer_properties(cluster, tenant, layer, entity, tag_json) \ No newline at end of file + return set_env_layer_properties(cluster, tenant, layer, entity, tag_json) diff --git a/tests/mockserver_payloads/requests/hosts/tags.json b/tests/mockserver_payloads/requests/hosts/tags.json new file mode 100644 index 0000000..5694743 --- /dev/null +++ b/tests/mockserver_payloads/requests/hosts/tags.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "demo", + "example" + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/maintenance/mock_create_daily_1.json b/tests/mockserver_payloads/requests/maintenance/mock_create_daily_1.json index d740f54..97a5743 100644 --- a/tests/mockserver_payloads/requests/maintenance/mock_create_daily_1.json +++ b/tests/mockserver_payloads/requests/maintenance/mock_create_daily_1.json @@ -1,5 +1,5 @@ { - "name":"Test Payload Daily", + "name":"Test Payload", "description":"Generating Payload for Test", "suppression":"DETECT_PROBLEMS_AND_ALERT", "schedule":{ diff --git a/tests/mockserver_payloads/requests/maintenance/mock_create_daily_multi_tags_and_1.json b/tests/mockserver_payloads/requests/maintenance/mock_create_daily_multi_tags_and_1.json new file mode 100644 index 0000000..7299850 --- /dev/null +++ b/tests/mockserver_payloads/requests/maintenance/mock_create_daily_multi_tags_and_1.json @@ -0,0 +1,28 @@ +{ + "name": "Test Payload", + "description": "Generating Payload for Test", + "suppression": "DETECT_PROBLEMS_AND_ALERT", + "schedule": { + "recurrenceType": "DAILY", + "start": "2020-01-01 00:00", + "end": "2020-01-02 00:00", + "zoneId": "America/Chicago", + "recurrence": { + "startTime": "23:00", + "durationMinutes": 60 + } + }, + "type": "PLANNED", + "scope": { + "entities": [], + "matches": [{ + "tags": [{ + "context": "CONTEXTLESS", + "key": "testing" + }, { + "context": "CONTEXTLESS", + "key": "testing2" + }] + }] + } +} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/maintenance/mock_create_daily_multi_tags_or_1.json b/tests/mockserver_payloads/requests/maintenance/mock_create_daily_multi_tags_or_1.json new file mode 100644 index 0000000..77ade49 --- /dev/null +++ b/tests/mockserver_payloads/requests/maintenance/mock_create_daily_multi_tags_or_1.json @@ -0,0 +1,30 @@ +{ + "name": "Test Payload", + "description": "Generating Payload for Test", + "suppression": "DETECT_PROBLEMS_AND_ALERT", + "schedule": { + "recurrenceType": "DAILY", + "start": "2020-01-01 00:00", + "end": "2020-01-02 00:00", + "zoneId": "America/Chicago", + "recurrence": { + "startTime": "23:00", + "durationMinutes": 60 + } + }, + "type": "PLANNED", + "scope": { + "entities": [], + "matches": [{ + "tags": [{ + "context": "CONTEXTLESS", + "key": "testing" + }] + }, { + "tags": [{ + "context": "CONTEXTLESS", + "key": "testing2" + }] + }] + } +} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/maintenance/mock_create_daily_single_tag_1.json b/tests/mockserver_payloads/requests/maintenance/mock_create_daily_single_tag_1.json index a309fb1..63d1348 100644 --- a/tests/mockserver_payloads/requests/maintenance/mock_create_daily_single_tag_1.json +++ b/tests/mockserver_payloads/requests/maintenance/mock_create_daily_single_tag_1.json @@ -1,5 +1,5 @@ { - "name": "Test Payload Daily with Tag", + "name": "Test Payload", "description": "Generating Payload for Test", "suppression": "DETECT_PROBLEMS_AND_ALERT", "schedule": { diff --git a/tests/mockserver_payloads/requests/maintenance/mock_create_monthly_1.json b/tests/mockserver_payloads/requests/maintenance/mock_create_monthly_1.json new file mode 100644 index 0000000..5e0ddf6 --- /dev/null +++ b/tests/mockserver_payloads/requests/maintenance/mock_create_monthly_1.json @@ -0,0 +1,17 @@ +{ + "name": "Test Payload", + "description": "Generating Payload for Test", + "suppression": "DETECT_PROBLEMS_AND_ALERT", + "schedule": { + "recurrenceType": "MONTHLY", + "start": "2020-01-01 00:00", + "end": "2020-01-02 00:00", + "zoneId": "America/Chicago", + "recurrence": { + "startTime": "23:00", + "durationMinutes": 60, + "dayOfMonth": 1 + } + }, + "type": "PLANNED" +} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/maintenance/mock_create_once_1.json b/tests/mockserver_payloads/requests/maintenance/mock_create_once_1.json new file mode 100644 index 0000000..b849cf5 --- /dev/null +++ b/tests/mockserver_payloads/requests/maintenance/mock_create_once_1.json @@ -0,0 +1,12 @@ +{ + "name": "Test Payload", + "description": "Generating Payload for Test", + "suppression": "DETECT_PROBLEMS_AND_ALERT", + "schedule": { + "recurrenceType": "ONCE", + "start": "2020-01-01 00:00", + "end": "2020-01-02 00:00", + "zoneId": "America/Chicago" + }, + "type": "PLANNED" +} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/maintenance/mock_create_weekly_1.json b/tests/mockserver_payloads/requests/maintenance/mock_create_weekly_1.json new file mode 100644 index 0000000..2639fa9 --- /dev/null +++ b/tests/mockserver_payloads/requests/maintenance/mock_create_weekly_1.json @@ -0,0 +1,17 @@ +{ + "name": "Test Payload", + "description": "Generating Payload for Test", + "suppression": "DETECT_PROBLEMS_AND_ALERT", + "schedule": { + "recurrenceType": "WEEKLY", + "start": "2020-01-01 00:00", + "end": "2020-01-02 00:00", + "zoneId": "America/Chicago", + "recurrence": { + "startTime": "23:00", + "durationMinutes": 60, + "dayOfWeek": "SUNDAY" + } + }, + "type": "PLANNED" +} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/processes/tags.json b/tests/mockserver_payloads/requests/processes/tags.json new file mode 100644 index 0000000..dc89ff6 --- /dev/null +++ b/tests/mockserver_payloads/requests/processes/tags.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "demo", + "example" + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/services/tags.json b/tests/mockserver_payloads/requests/services/tags.json new file mode 100644 index 0000000..dc89ff6 --- /dev/null +++ b/tests/mockserver_payloads/requests/services/tags.json @@ -0,0 +1,6 @@ +{ + "tags": [ + "demo", + "example" + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/hosts/get_all.json b/tests/mockserver_payloads/responses/hosts/get_all.json new file mode 100644 index 0000000..bae3691 --- /dev/null +++ b/tests/mockserver_payloads/responses/hosts/get_all.json @@ -0,0 +1,14 @@ +[ + { + "entityId": "HOST-ABC123DEF456GHIJ", + "consumedHostUnits": 0.25 + }, + { + "entityId": "HOST-5B9CE4E4E14185FA", + "consumedHostUnits": 0.25 + }, + { + "entityId": "HOST-421D60DB4A2EA929", + "consumedHostUnits": 3.5 + } +] \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/hosts/get_single.json b/tests/mockserver_payloads/responses/hosts/get_single.json new file mode 100644 index 0000000..b27e2e7 --- /dev/null +++ b/tests/mockserver_payloads/responses/hosts/get_single.json @@ -0,0 +1,4 @@ +{ + "entityId": "HOST-ABC123DEF456GHIJ", + "consumedHostUnits": 0.25 +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/maintenance/mock_create_daily_1.json b/tests/mockserver_payloads/responses/maintenance/mock_create_1.json similarity index 75% rename from tests/mockserver_payloads/responses/maintenance/mock_create_daily_1.json rename to tests/mockserver_payloads/responses/maintenance/mock_create_1.json index 36d21d7..de86d44 100644 --- a/tests/mockserver_payloads/responses/maintenance/mock_create_daily_1.json +++ b/tests/mockserver_payloads/responses/maintenance/mock_create_1.json @@ -1,5 +1,5 @@ { "id": "1a000000-200a-3000-4000-5abc00000000", - "name": "Test Payload Daily", + "name": "Test Payload", "description": "Generating Payload for Test" } \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/maintenance/mock_create_daily_single_tag_1.json b/tests/mockserver_payloads/responses/maintenance/mock_create_daily_single_tag_1.json deleted file mode 100644 index 16ba0d1..0000000 --- a/tests/mockserver_payloads/responses/maintenance/mock_create_daily_single_tag_1.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "id": "f8d5614d-7407-4fdf-a6a1-1e0ed693a6cf", - "name": "Test Payload Daily with Tag", - "description": "Generating Payload for Test" -} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_all_pgis.json b/tests/mockserver_payloads/responses/processes/get_all_pgis.json new file mode 100644 index 0000000..121da81 --- /dev/null +++ b/tests/mockserver_payloads/responses/processes/get_all_pgis.json @@ -0,0 +1,11 @@ +[ + { + "entityId": "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" + }, + { + "entityId": "PROCESS_GROUP_INSTANCE-A6AAFEA17E6F60FD" + }, + { + "entityId": "PROCESS_GROUP_INSTANCE-F0967E6BFEE20424" + } +] \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_all_pgs.json b/tests/mockserver_payloads/responses/processes/get_all_pgs.json new file mode 100644 index 0000000..73e086a --- /dev/null +++ b/tests/mockserver_payloads/responses/processes/get_all_pgs.json @@ -0,0 +1,11 @@ +[ + { + "entityId": "PROCESS_GROUP-ABC123DEF456GHI7" + }, + { + "entityId": "PROCESS_GROUP-19DACA5E22637C33" + }, + { + "entityId": "PROCESS_GROUP-859E1549052CD876" + } +] \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_one_pg.json b/tests/mockserver_payloads/responses/processes/get_one_pg.json new file mode 100644 index 0000000..14223a1 --- /dev/null +++ b/tests/mockserver_payloads/responses/processes/get_one_pg.json @@ -0,0 +1,3 @@ +{ + "entityId": "PROCESS_GROUP-ABC123DEF456GHI7" +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/processes/get_one_pgi.json b/tests/mockserver_payloads/responses/processes/get_one_pgi.json new file mode 100644 index 0000000..0898df1 --- /dev/null +++ b/tests/mockserver_payloads/responses/processes/get_one_pgi.json @@ -0,0 +1,3 @@ +{ + "entityId": "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/services/get_all.json b/tests/mockserver_payloads/responses/services/get_all.json new file mode 100644 index 0000000..bdcce80 --- /dev/null +++ b/tests/mockserver_payloads/responses/services/get_all.json @@ -0,0 +1,11 @@ +[ + { + "entityId": "SERVICE-ABC123DEF456GHI7" + }, + { + "entityId": "SERVICE-C096CE0BA471AEFD" + }, + { + "entityId": "SERVICE-B71ADA892013D156" + } +] \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/services/get_one.json b/tests/mockserver_payloads/responses/services/get_one.json new file mode 100644 index 0000000..f5c0619 --- /dev/null +++ b/tests/mockserver_payloads/responses/services/get_one.json @@ -0,0 +1,3 @@ +{ + "entityId": "SERVICE-ABC123DEF456GHI7" +} \ No newline at end of file diff --git a/tests/special_test_settings_no_variables.py b/tests/special_test_settings_no_variables.py new file mode 100644 index 0000000..e591020 --- /dev/null +++ b/tests/special_test_settings_no_variables.py @@ -0,0 +1,14 @@ +"""Special Cases for settings which requires user_variables.py to NOT exist""" +import unittest +from os import rename + + +class TestSettingsWithoutVarFile(unittest.TestCase): + """Special test cases when user_variables is absent""" + def test_settings_without_var_file(self): + """Test should return default value when user_variables missing""" + rename("user_variables.py", "user_variables.py.tmp") + from dynatrace import settings # pylint: disable=import-outside-toplevel + timezone = settings.get_setting("DEFAULT_TIMEZONE") + self.assertEqual(timezone, "UTC") + rename("user_variables.py.tmp", "user_variables.py") diff --git a/tests/test_host_groups.py b/tests/test_host_groups.py index 69581a3..9d3c4ae 100644 --- a/tests/test_host_groups.py +++ b/tests/test_host_groups.py @@ -1,6 +1,6 @@ """Testing dynatrace.tenant.host_groups""" import unittest -import user_variables +import user_variables # pylint: disable=import-error from tests import tooling_for_test from dynatrace.tenant import host_groups @@ -8,23 +8,34 @@ TENANT = "tenant1" URL_PATH = "/api/v1/entity/infrastructure/hosts" + class TestHostGroupFunctions(unittest.TestCase): - RESPONSE_DIR = "tests/mockserver_payloads/responses/host_groups/" - def test_get_host_groups_tenantwide(self): - parameters = { - "relativeTime": ["day"], - "includeDetails": [ "true" ], - "Api-Token": [CLUSTER["api_token"][TENANT]], - } - mockserver_response_file = f"{self.RESPONSE_DIR}mock_get_general_1.json" - tooling_for_test.create_mockserver_expectation( - CLUSTER, TENANT, URL_PATH, "GET", parameters=parameters, response_file=mockserver_response_file) - command_tested = host_groups.get_host_groups_tenantwide(CLUSTER, TENANT) + """General Tests for Host Group Functions""" + RESPONSE_DIR = "tests/mockserver_payloads/responses/host_groups/" + + def test_get_host_groups_tenantwide(self): + """Testing Retreival of all Host Groups within a single tenant""" + parameters = { + "relativeTime": ["day"], + "includeDetails": ["true"], + } + mockserver_response_file = f"{self.RESPONSE_DIR}mock_get_general_1.json" + tooling_for_test.create_mockserver_expectation( + CLUSTER, + TENANT, + URL_PATH, + "GET", + parameters=parameters, + response_file=mockserver_response_file + ) + command_tested = host_groups.get_host_groups_tenantwide( + CLUSTER, TENANT) + + expected_result = { + 'HOST_GROUP-ABCDEFGH12345678': 'HOST_GROUP_1' + } + self.assertEqual(command_tested, expected_result) - expected_result = { - 'HOST_GROUP-ABCDEFGH12345678': 'HOST_GROUP_1' - } - self.assertEqual(command_tested, expected_result) if __name__ == '__main__': - unittest.main() + unittest.main() diff --git a/tests/test_maintenance_windows.py b/tests/test_maintenance_windows.py index 1142951..b1d3a3f 100644 --- a/tests/test_maintenance_windows.py +++ b/tests/test_maintenance_windows.py @@ -1,24 +1,22 @@ -""" -Test Cases For Maintenance Windows. -""" +"""Test Cases For Maintenance Windows.""" import unittest -import user_variables +import user_variables # pylint: disable=import-error from tests import tooling_for_test from dynatrace.tenant import maintenance from dynatrace.requests.request_handler import TenantAPIs +from dynatrace.exceptions import InvalidDateFormatException CLUSTER = user_variables.FULL_SET["mockserver1"] TENANT = "tenant1" -URL_PATH = TenantAPIs.MAINTENANCE_WINDOWS +URL_PATH = str(TenantAPIs.MAINTENANCE_WINDOWS) +TEST_RANGE_START = "2020-01-01 00:00" +TEST_RANGE_END = "2020-01-02 00:00" +TEST_PAYLOAD_TITLE = "Test Payload" +TEST_PAYLOAD_DESC = "Generating Payload for Test" class TestMaintenanceWindowCreate(unittest.TestCase): - """ - Test Cases for Creating a Maintenance Window - - Args: - unittest ([type]): [description] - """ + """Test Cases for Creating a Maintenance Window""" REQUEST_DIR = "tests/mockserver_payloads/requests/maintenance/" RESPONSE_DIR = "tests/mockserver_payloads/responses/maintenance/" @@ -27,7 +25,7 @@ def test_create_daily_no_scope(self): Testing create daily Maintenance Window with no scope """ mockserver_request_file = f"{self.REQUEST_DIR}mock_create_daily_1.json" - mockserver_response_file = f"{self.RESPONSE_DIR}mock_create_daily_1.json" + mockserver_response_file = f"{self.RESPONSE_DIR}mock_create_1.json" tooling_for_test.create_mockserver_expectation( CLUSTER, TENANT, @@ -37,28 +35,170 @@ def test_create_daily_no_scope(self): response_file=mockserver_response_file, ) maintenance_schedule = maintenance.generate_schedule( - "DAILY", + maintenance.RecurrenceType.DAILY, "23:00", 60, - "2020-01-01 00:00", - "2020-01-02 00:00" + TEST_RANGE_START, + TEST_RANGE_END ) maintenance_json = maintenance.generate_window_json( - "Test Payload Daily", - "Generating Payload for Test", - "DETECT_PROBLEMS_AND_ALERT", + TEST_PAYLOAD_TITLE, + TEST_PAYLOAD_DESC, + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT, maintenance_schedule, is_planned=True ) result = maintenance.create_window(CLUSTER, TENANT, maintenance_json) - self.assertEqual(result, tooling_for_test.expected_payload(mockserver_response_file)) + self.assertEqual(result, tooling_for_test.expected_payload( + mockserver_response_file)) def test_create_daily_single_tag(self): - """ - Testing create daily Maintenance Window with a single tag scope - """ + """Testing create daily Maintenance Window with a single tag scope""" mockserver_request_file = f"{self.REQUEST_DIR}mock_create_daily_single_tag_1.json" - mockserver_response_file = f"{self.RESPONSE_DIR}mock_create_daily_single_tag_1.json" + mockserver_response_file = f"{self.RESPONSE_DIR}mock_create_1.json" + tooling_for_test.create_mockserver_expectation( + CLUSTER, + TENANT, + URL_PATH, + "POST", + request_file=mockserver_request_file, + response_file=mockserver_response_file, + ) + maintenance_schedule = maintenance.generate_schedule( + maintenance.RecurrenceType.DAILY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END + ) + maintenance_scope = maintenance.generate_scope( + tags=[{'context': "CONTEXTLESS", 'key': "testing"}]) + maintenance_json = maintenance.generate_window_json( + TEST_PAYLOAD_TITLE, + TEST_PAYLOAD_DESC, + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT, + maintenance_schedule, + scope=maintenance_scope, + is_planned=True + ) + result = maintenance.create_window(CLUSTER, TENANT, maintenance_json) + self.assertEqual(result, tooling_for_test.expected_payload( + mockserver_response_file)) + + def test_create_daily_tags_and(self): + """Testing Payloads with multiple tags in an \"AND\" configuration""" + mockserver_request_file = f"{self.REQUEST_DIR}mock_create_daily_multi_tags_and_1.json" + mockserver_response_file = f"{self.RESPONSE_DIR}mock_create_1.json" + + tooling_for_test.create_mockserver_expectation( + CLUSTER, + TENANT, + URL_PATH, + "POST", + request_file=mockserver_request_file, + response_file=mockserver_response_file, + ) + maintenance_schedule = maintenance.generate_schedule( + maintenance.RecurrenceType.DAILY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END + ) + maintenance_scope = maintenance.generate_scope( + tags=[ + {'context': "CONTEXTLESS", 'key': "testing"}, + {'context': "CONTEXTLESS", 'key': "testing2"} + ], + match_any_tag=False + ) + maintenance_json = maintenance.generate_window_json( + TEST_PAYLOAD_TITLE, + TEST_PAYLOAD_DESC, + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT, + maintenance_schedule, + scope=maintenance_scope, + is_planned=True + ) + result = maintenance.create_window(CLUSTER, TENANT, maintenance_json) + self.assertEqual(result, tooling_for_test.expected_payload( + mockserver_response_file)) + + def test_create_daily_tags_or(self): + """Testing Payloads with multiple tags in an \"AND\" configuration""" + mockserver_request_file = f"{self.REQUEST_DIR}mock_create_daily_multi_tags_or_1.json" + mockserver_response_file = f"{self.RESPONSE_DIR}mock_create_1.json" + + tooling_for_test.create_mockserver_expectation( + CLUSTER, + TENANT, + URL_PATH, + "POST", + request_file=mockserver_request_file, + response_file=mockserver_response_file, + ) + maintenance_schedule = maintenance.generate_schedule( + maintenance.RecurrenceType.DAILY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END + ) + maintenance_scope = maintenance.generate_scope( + tags=[ + {'context': "CONTEXTLESS", 'key': "testing"}, + {'context': "CONTEXTLESS", 'key': "testing2"} + ], + match_any_tag=True + ) + maintenance_json = maintenance.generate_window_json( + TEST_PAYLOAD_TITLE, + TEST_PAYLOAD_DESC, + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT, + maintenance_schedule, + scope=maintenance_scope, + is_planned=True + ) + result = maintenance.create_window(CLUSTER, TENANT, maintenance_json) + self.assertEqual(result, tooling_for_test.expected_payload( + mockserver_response_file)) + + def test_create_once_no_scope(self): + """Testing Payloads with ONCE recurrance type""" + mockserver_request_file = f"{self.REQUEST_DIR}mock_create_once_1.json" + mockserver_response_file = f"{self.RESPONSE_DIR}mock_create_1.json" + + tooling_for_test.create_mockserver_expectation( + CLUSTER, + TENANT, + URL_PATH, + "POST", + request_file=mockserver_request_file, + response_file=mockserver_response_file, + ) + maintenance_schedule = maintenance.generate_schedule( + maintenance.RecurrenceType.ONCE, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END + ) + maintenance_json = maintenance.generate_window_json( + TEST_PAYLOAD_TITLE, + TEST_PAYLOAD_DESC, + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT, + maintenance_schedule, + is_planned=True + ) + result = maintenance.create_window(CLUSTER, TENANT, maintenance_json) + self.assertEqual(result, tooling_for_test.expected_payload( + mockserver_response_file)) + + def test_create_weekly_no_scope(self): + """Testing Payloads with WEEKLY recurrance type""" + mockserver_request_file = f"{self.REQUEST_DIR}mock_create_weekly_1.json" + mockserver_response_file = f"{self.RESPONSE_DIR}mock_create_1.json" + tooling_for_test.create_mockserver_expectation( CLUSTER, TENANT, @@ -68,23 +208,292 @@ def test_create_daily_single_tag(self): response_file=mockserver_response_file, ) maintenance_schedule = maintenance.generate_schedule( - "DAILY", + maintenance.RecurrenceType.WEEKLY, "23:00", 60, - "2020-01-01 00:00", - "2020-01-02 00:00" + TEST_RANGE_START, + TEST_RANGE_END, + day=maintenance.DayOfWeek.SUNDAY ) - maintenance_scope = maintenance.generate_scope(tags=[{'context': "CONTEXTLESS",'key': "testing"}]) maintenance_json = maintenance.generate_window_json( - "Test Payload Daily", - "Generating Payload for Test", - "DETECT_PROBLEMS_AND_ALERT", + TEST_PAYLOAD_TITLE, + TEST_PAYLOAD_DESC, + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT, maintenance_schedule, - scope= maintenance_scope, is_planned=True ) result = maintenance.create_window(CLUSTER, TENANT, maintenance_json) - self.assertEqual(result, tooling_for_test.expected_payload(mockserver_response_file)) + self.assertEqual(result, tooling_for_test.expected_payload( + mockserver_response_file)) + + def test_create_monthly_no_scope(self): + """Testing Payloads with MONTHLY recurrance type""" + mockserver_request_file = f"{self.REQUEST_DIR}mock_create_monthly_1.json" + mockserver_response_file = f"{self.RESPONSE_DIR}mock_create_1.json" + + tooling_for_test.create_mockserver_expectation( + CLUSTER, + TENANT, + URL_PATH, + "POST", + request_file=mockserver_request_file, + response_file=mockserver_response_file, + ) + maintenance_schedule = maintenance.generate_schedule( + maintenance.RecurrenceType.MONTHLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + day=1 + ) + maintenance_json = maintenance.generate_window_json( + TEST_PAYLOAD_TITLE, + TEST_PAYLOAD_DESC, + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT, + maintenance_schedule, + is_planned=True + ) + result = maintenance.create_window(CLUSTER, TENANT, maintenance_json) + self.assertEqual(result, tooling_for_test.expected_payload( + mockserver_response_file)) + + +class TestMaintenanceExceptions(unittest.TestCase): + """Series of Tests aimed at triggering exception""" + def test_invalid_recurrence_type(self): + """Testing exception thrown for invalid recurrence type""" + with self.assertRaises(ValueError) as context: + maintenance.generate_schedule( + "HOURLY", + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + ) + + self.assertTrue("Invalid Recurrence Type!" in str(context.exception)) + + def test_invalid_day_of_week(self): + """Testing exception thrown for invalid dayOfWeek""" + with self.assertRaises(ValueError) as context: + maintenance.generate_schedule( + maintenance.RecurrenceType.WEEKLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + day=1 + ) + self.assertTrue("Invalid Weekly Day!" in str(context.exception)) + + def test_invalid_day_of_month_value(self): + """Testing exception thrown for invalid dayOfMonth for incorrect int""" + with self.assertRaises(ValueError) as context: + maintenance.generate_schedule( + maintenance.RecurrenceType.MONTHLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + day=32 + ) + self.assertTrue("Invalid Monthly Day!" in str(context.exception)) + + def test_invalid_day_of_month_type(self): + """Testing exception thrown for invalid dayOfMonth for a non-int""" + with self.assertRaises(TypeError) as context: + maintenance.generate_schedule( + maintenance.RecurrenceType.MONTHLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + day="Eleven" + ) + self.assertTrue( + "Invalid type for Day of Month! Int between 1-31 required" in str(context.exception)) + + def test_no_day_of_week_supplied(self): + """Weekly Maintenance Window with no dayOfWeek supplied""" + with self.assertRaises(Exception) as context: + maintenance.generate_schedule( + maintenance.RecurrenceType.WEEKLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + ) + self.assertTrue("Invalid Weekly Day!" in str(context.exception)) + + def test_no_day_of_month_supplied(self): + """Monthly Maintenance Window with no dayOfMonth supplied""" + with self.assertRaises(Exception) as context: + maintenance.generate_schedule( + maintenance.RecurrenceType.MONTHLY, + "23:00", + 60, + TEST_RANGE_START, + TEST_RANGE_END, + ) + self.assertTrue( + "Invalid type for Day of Month!" in str(context.exception)) + + def test_invalid_datetime_format(self): + """Test invalid datetime supplied to trigger ValueError""" + with self.assertRaises(InvalidDateFormatException) as context: + maintenance.generate_schedule( + maintenance.RecurrenceType.DAILY, + "23:00", + 60, + TEST_RANGE_START, + "2020-01-02" + ) + self.assertTrue( + "Incorrect Date " in context.exception.message, context.exception.message) + + def test_invalid_filter_type(self): + """Invalid Filter_Type""" + with self.assertRaises(ValueError) as context: + maintenance.generate_scope( + tags=[{'context': "CONTEXTLESS", 'key': "testing"}], + filter_type="INVALID_TYPE" + ) + self.assertTrue("Invalid Filter Type" in ( + msg := str(context.exception)), msg) # pylint: disable=used-before-assignment + + +class TestMaintenanceEnumTypes(unittest.TestCase): + """Test to validate Maintenance Enum Types are correct""" + def test_suppression_enum_str(self): + """Suppression enum str should be string""" + suppression = maintenance.Suppression( + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) + self.assertIsInstance( + maintenance.Suppression.__str__(suppression), str) + + def test_suppression_enum_repr(self): + """Suppression enum repr should be string""" + suppression = maintenance.Suppression( + maintenance.Suppression.DETECT_PROBLEMS_AND_ALERT) + self.assertIsInstance( + maintenance.Suppression.__repr__(suppression), str) + + def test_day_of_week_enum_str(self): + """Day of Week enum str should be string""" + day_of_week = maintenance.DayOfWeek(maintenance.DayOfWeek.MONDAY) + self.assertIsInstance(maintenance.DayOfWeek.__str__(day_of_week), str) + + def test_day_of_week_enum_repr(self): + """Day of Week enum repr should be string""" + day_of_week = maintenance.DayOfWeek(maintenance.DayOfWeek.MONDAY) + self.assertIsInstance(maintenance.DayOfWeek.__repr__(day_of_week), str) + + def test_context_enum_str(self): + """Context enum str should be string""" + context = maintenance.Context(maintenance.Context.CONTEXTLESS) + self.assertIsInstance(maintenance.Context.__str__(context), str) + + def test_context_enum_repr(self): + """Context enum repr should be string""" + context = maintenance.Context(maintenance.Context.CONTEXTLESS) + self.assertIsInstance(maintenance.Context.__repr__(context), str) + + def test_recurrence_type_enum_str(self): + """Recurrence Type enum str should be string""" + recurrence_type = maintenance.RecurrenceType( + maintenance.RecurrenceType.DAILY) + self.assertIsInstance( + maintenance.RecurrenceType.__str__(recurrence_type), str) + + def test_recurrence_type_enum_repr(self): + """Recurrence Type enum repr should be string""" + recurrence_type = maintenance.RecurrenceType( + maintenance.RecurrenceType.DAILY) + self.assertIsInstance( + maintenance.RecurrenceType.__repr__(recurrence_type), str) + + def test_filter_type_enum_str(self): + """Filter Type enum str should be string""" + suppression = maintenance.FilterType( + maintenance.FilterType.APM_SECURITY_GATEWAY) + self.assertIsInstance(maintenance.FilterType.__str__(suppression), str) + + def test_filter_type_enum_repr(self): + """Filter Type enum repr should be string""" + suppression = maintenance.FilterType( + maintenance.FilterType.APM_SECURITY_GATEWAY) + self.assertIsInstance( + maintenance.FilterType.__repr__(suppression), str) + + +class TestTagParsing(unittest.TestCase): + """Testing Maintenance Window Tag Handling""" + def test_tag_variations(self): + """Testing various ways tags need to be parsed""" + # Test 1 - Key + # Test 2 - Key, Value + # Test 3 - Context, Key and Value + # Test 4 - Key with Colon, Value + # Test 5 - Key with Colon, Value Blank + # Test 6 - Context, Key with Colon and Value + # Test 7 - Context, Key + # Test 8 - Context, Key with square brackets + # Test 9 - Context, Key with colon and squares + # Test 10 - Empty Context with squares + + test_tag_list = [ + "Key", + "Key:Value", + "[Context]Key:Value", + "Key:withColon:Value", + "Key:withColon:", + "[Context]Key:withColon:Value", + "[Context]Key", + "[Context][KeywithSquares]", + "[Context][KeyWithSquares]:AndColons:Value", + "[][KeywithSquares]", + ] + + test_tag_expected_results = [ + {'context': 'CONTEXTLESS', 'key': 'Key'}, + {'context': 'CONTEXTLESS', 'key': 'Key:Value'}, + {'context': 'Context', 'key': 'Key:Value'}, + {'context': 'CONTEXTLESS', 'key': 'Key:withColon:Value'}, + {'context': 'CONTEXTLESS', 'key': 'Key:withColon:'}, + {'context': 'Context', 'key': 'Key:withColon:Value'}, + {'context': 'Context', 'key': 'Key'}, + {'context': 'Context', 'key': '[KeywithSquares]'}, + {'context': 'Context', + 'key': '[KeyWithSquares]:AndColons:Value'}, + {'context': 'CONTEXTLESS', 'key': '[][KeywithSquares]'}, + ] + + for i, test_tag_input in enumerate(test_tag_list): + processed_tag = test_tag_input + self.assertTrue( + (result := maintenance.parse_tag(processed_tag) + ) == test_tag_expected_results[i], + f"Test {i}: {result} did not match {test_tag_expected_results[i]}") + if __name__ == '__main__': unittest.main() + +# CREATE TESTS LEFT: +# Single Entity +# Multi Entity +# Single Tag with Filter Type +# Mutli Tags with Filter Type +# Single Tag with Management Zone +# Multi Tags with Management Zone + +# EXCEPTION TEST CASES: +# MANAGEMENT_ZONE WITHOUT TAG +# FILTER_TYPE WITHOUT TAG + +# OTHER TEST CASES: +# GET ALL WINDOWS +# GET DETAILS OF WINDOW +# DELETE WINDOW +# UPDATE WINDOW diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..4f81d8e --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,24 @@ +"""Test Cases for dynatrace.settings""" +import unittest +from dynatrace import settings + + +class TestSettings(unittest.TestCase): + """Standard Testing Class""" + def test_get_setting_from_user_variable(self): + """Will pull from user_variables when available""" + timezone = settings.get_setting("DEFAULT_TIMEZONE") + self.assertEqual(timezone, "America/Chicago") + + def test_get_setting_from_default(self): + """When not in user_variables, info should be pulled from default values""" + log_dir = settings.get_setting("LOG_DIR") + self.assertEqual(log_dir, "logs/") + + def test_get_invalid_setting(self): + """"When not a valid setting, an error should be thrown""" + with self.assertRaises(AttributeError) as context: + settings.get_setting("INVALID_SETTING") + + self.assertIn("not a valid user variable attribute", + str(context.exception)) diff --git a/tests/test_topology_hosts.py b/tests/test_topology_hosts.py new file mode 100644 index 0000000..c25af78 --- /dev/null +++ b/tests/test_topology_hosts.py @@ -0,0 +1,127 @@ +""" +Test Suite for Topology Hosts +""" +import unittest +from user_variables import FULL_SET # pylint: disable=import-error +from tests import tooling_for_test as testtools +from dynatrace.requests.request_handler import TenantAPIs +from dynatrace.tenant.topology import hosts + +CLUSTER = FULL_SET["mockserver1"] +TENANT = "tenant1" +URL_PATH = f"{TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts" +REQUEST_DIR = "tests/mockserver_payloads/requests/hosts" +RESPONSE_DIR = "tests/mockserver_payloads/responses/hosts" + + +class TestGetHosts(unittest.TestCase): + """Tests cases for fetching topology hosts.""" + + def test_get_all_hosts(self): + """Test fetching all hosts""" + + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file + ) + + result = hosts.get_hosts_tenantwide(CLUSTER, TENANT) + self.assertEqual(result, testtools.expected_payload(response_file)) + + def test_get_single_host(self): + """Test fetching a specific host""" + + host_id = "HOST-9F74450267BAAE20" + response_file = f"{RESPONSE_DIR}/get_single.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{host_id}", + request_type="GET", + response_file=response_file + ) + + result = hosts.get_host(CLUSTER, TENANT, host_id) + self.assertEqual(result, testtools.expected_payload(response_file)) + + def test_get_host_count(self): + """Test getting the count of hosts in a tenant.""" + + response_file = f"{RESPONSE_DIR}/get_all.json" + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file, + parameters=dict(relativeTime=['day'], + includeDetails=['False']) + ) + + result = hosts.get_host_count_tenantwide(CLUSTER, TENANT) + self.assertEqual(result, 3) + + def test_get_host_units(self): + """Tests getting the consumed host units in a tenant.""" + + response_file = f"{RESPONSE_DIR}/get_all.json" + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file + ) + + result = hosts.get_host_units_tenantwide(CLUSTER, TENANT) + self.assertEqual(result, 4) + + +class TestHostTagging(unittest.TestCase): + """Test cases for testing host-level tagging.""" + + def test_add_tags(self): + """Test adding two tags to a specific host.""" + + host_id = "HOST-ABC123DEF456GHIJ" + request_file = f"{REQUEST_DIR}/tags.json" + tags = ["demo", "example"] + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + request_type="POST", + url_path=f"{URL_PATH}/{host_id}", + request_file=request_file, + response_code=201 + ) + + result = hosts.add_host_tags(CLUSTER, TENANT, host_id, tags) + self.assertEqual(result, 201) + + def test_delete_tags(self): + """Test deleting a tag from a specific host.""" + + host_id = "HOST-ABC123DEF456GHIJ" + tag = "demo" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{host_id}/tags/{tag}", + request_type="DELETE", + response_code=204 + ) + + result = hosts.delete_host_tag(CLUSTER, TENANT, host_id, tag) + self.assertEqual(204, result.status_code) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_topology_process_groups.py b/tests/test_topology_process_groups.py new file mode 100644 index 0000000..0d6c099 --- /dev/null +++ b/tests/test_topology_process_groups.py @@ -0,0 +1,91 @@ +"""Test Suite for Topology Process Groups""" + +import unittest +from user_variables import FULL_SET # pylint: disable=import-error +from tests import tooling_for_test as testtools +from dynatrace.requests.request_handler import TenantAPIs +from dynatrace.tenant.topology import process_groups + +CLUSTER = FULL_SET.get('mockserver1') +TENANT = 'tenant1' +URL_PATH = f"{TenantAPIs.V1_TOPOLOGY}/infrastructure/process-groups" +REQUEST_DIR = "tests/mockserver_payloads/requests/processes" +RESPONSE_DIR = "tests/mockserver_payloads/responses/processes" + + +class TestGetPGs(unittest.TestCase): + """Test cases for fetching topology process groups.""" + + def test_get_all_pgs(self): + """Test fetching all PGs""" + response_file = f"{RESPONSE_DIR}/get_all_pgs.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file + ) + + result = process_groups.get_process_groups_tenantwide(CLUSTER, TENANT) + self.assertEqual(result, testtools.expected_payload(response_file)) + + def test_get_single_pg(self): + """Test fetching single PG""" + response_file = f"{RESPONSE_DIR}/get_one_pg.json" + pg_id = "PROCESS_GROUP-ABC123DEF456GHI7" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{pg_id}", + request_type="GET", + response_file=response_file + ) + + result = process_groups.get_process_group(CLUSTER, TENANT, pg_id) + self.assertEqual(result, testtools.expected_payload(response_file)) + + def test_get_pg_count(self): + """Test getting the PG count tenantwide.""" + response_file = f"{RESPONSE_DIR}/get_all_pgs.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file + ) + + result = process_groups.get_process_group_count_tenantwide(CLUSTER, + TENANT) + self.assertEqual(result, 3) + + +class TestPGTags(unittest.TestCase): + """Test cases for PG tags""" + + def test_add_pg_tags(self): + """Test adding two tags to the PG.""" + pg_id = "PROCESS_GROUP-859E1549052CD876" + request_file = f"{REQUEST_DIR}/tags.json" + tags = ["demo", "example"] + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + request_type="POST", + url_path=f"{URL_PATH}/{pg_id}", + request_file=request_file, + response_code=201 + ) + + result = process_groups.add_process_group_tags(CLUSTER, TENANT, + pg_id, tags) + self.assertEqual(result, 201) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_topology_processes.py b/tests/test_topology_processes.py new file mode 100644 index 0000000..e9c48a9 --- /dev/null +++ b/tests/test_topology_processes.py @@ -0,0 +1,52 @@ +"""Test suite for Topology Processes""" + +import unittest +from tests import tooling_for_test as testtools +from dynatrace import settings +from dynatrace.requests.request_handler import TenantAPIs +from dynatrace.tenant.topology import process + +FULL_SET = settings.get_setting("FULL_SET") +CLUSTER = FULL_SET.get('mockserver1') +TENANT = 'tenant1' +URL_PATH = f"{TenantAPIs.V1_TOPOLOGY}/infrastructure/processes" +RESPONSE_DIR = "tests/mockserver_payloads/responses/processes" + + +class TestGetProcesses(unittest.TestCase): + """Test cases for fetching topology processes.""" + + def test_get_all_processes(self): + """Test getting all processes tenantwide.""" + response_file = f"{RESPONSE_DIR}/get_all_pgis.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file + ) + + result = process.get_processes_tenantwide(CLUSTER, TENANT) + self.assertEqual(result, testtools.expected_payload(response_file)) + + def test_get_single_process(self): + """Tests getting one specific process.""" + response_file = f"{RESPONSE_DIR}/get_one_pgi.json" + process_id = "PROCESS_GROUP_INSTANCE-ABC123DEF456GHI7" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{process_id}", + request_type="GET", + response_file=response_file + ) + + result = process.get_process(CLUSTER, TENANT, process_id) + self.assertEqual(result, testtools.expected_payload(response_file)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_topology_services.py b/tests/test_topology_services.py new file mode 100644 index 0000000..115f2ee --- /dev/null +++ b/tests/test_topology_services.py @@ -0,0 +1,89 @@ +"""Test Suite for Topology Services""" + +import unittest +from user_variables import FULL_SET # pylint: disable=import-error +from tests import tooling_for_test as testtools +from dynatrace.requests.request_handler import TenantAPIs +from dynatrace.tenant.topology import services + +CLUSTER = FULL_SET.get('mockserver1') +TENANT = 'tenant1' +URL_PATH = f"{TenantAPIs.V1_TOPOLOGY}/infrastructure/services" +REQUEST_DIR = "tests/mockserver_payloads/requests/services" +RESPONSE_DIR = "tests/mockserver_payloads/responses/services" + + +class TestGetServices(unittest.TestCase): + """Test cases for fetching topology services.""" + + def test_get_all_svc(self): + """Test fetching all services""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file + ) + + result = services.get_services_tenantwide(CLUSTER, TENANT) + self.assertEqual(result, testtools.expected_payload(response_file)) + + def test_get_single_svc(self): + """Test fetching single service""" + response_file = f"{RESPONSE_DIR}/get_one.json" + svc_id = "SERVICE-ABC123DEF456GHI7" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{svc_id}", + request_type="GET", + response_file=response_file + ) + + result = services.get_service(CLUSTER, TENANT, svc_id) + self.assertEqual(result, testtools.expected_payload(response_file)) + + def test_get_svc_count(self): + """Test getting the service count tenantwide.""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + response_file=response_file + ) + + result = services.get_service_count_tenantwide(CLUSTER, TENANT) + self.assertEqual(result, 3) + + +class TestServiceTags(unittest.TestCase): + """Test cases for service tags""" + + def test_add_svc_tags(self): + """Test adding two tags to the service.""" + svc_id = "SERVICE-ABC123DEF456GHI7" + request_file = f"{REQUEST_DIR}/tags.json" + tags = ["demo", "example"] + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + request_type="POST", + url_path=f"{URL_PATH}/{svc_id}", + request_file=request_file, + response_code=201 + ) + + result = services.add_service_tags(CLUSTER, TENANT, svc_id, tags) + self.assertEqual(result, 201) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index bc612e2..63b2275 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -1,19 +1,32 @@ """Mockserver Expectation Setup""" -import requests import json import logging +import requests from dynatrace.requests.request_handler import generate_tenant_url logging.basicConfig(filename="testing_tools.log", level=logging.DEBUG) def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwargs): - requests.packages.urllib3.disable_warnings() + """Create Payload For MockServer to expect and respond + + Args: + cluster (Dictionary): [description] + tenant (str): [description] + url_path (str): [description] + request_type (HTTP str): [description] + + Raises: + ValueError: [description] + """ + requests.packages.urllib3.disable_warnings() # pylint: disable=no-member expectation = { "httpRequest": { - "queryStringParameters": { - "Api-Token": [cluster.get('api_token').get(tenant)] + "headers": { + "Authorization": [f"Api-Token {cluster.get('api_token').get(tenant)}"], }, + "path": url_path, + "method": request_type }, "httpResponse": { "statusCode": 200 @@ -25,22 +38,23 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa "id": "OneOff", } - logging.debug(f"KWARGS {kwargs}") + logging.debug("URL PATH: %s", url_path) + logging.debug("KWARGS %s", kwargs) # Paramaters should always at least have Api-Token if 'parameters' in kwargs: expectation["httpRequest"]["queryStringParameters"] = kwargs['parameters'] if "request_file" in kwargs: - with open(kwargs['request_file']) as f: - request_payload = json.load(f) + with open(kwargs['request_file']) as open_file: + request_payload = json.load(open_file) expectation["httpRequest"]["body"] = { "type": "JSON", "json": request_payload, } if "response_file" in kwargs: - with open(kwargs['response_file']) as f: - response_payload = json.load(f) + with open(kwargs['response_file']) as open_file: + response_payload = json.load(open_file) expectation["httpResponse"]["body"] = { "type": "JSON", "json": response_payload, @@ -50,7 +64,7 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa } if "response_code" in kwargs: - expectation['httpResponse']['statusCode'] = kwargs['response_code'] + expectation["httpResponse"]["statusCode"] = kwargs["response_code"] if "mock_id" in kwargs: expectation["id"] = kwargs["mock_id"] @@ -71,5 +85,13 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa def expected_payload(json_file): - with open(json_file) as f: - return json.load(f) + """The payload that should be tested against + + Args: + json_file (str): file name for result json + + Returns: + dict: payload of the expected result JSON + """ + with open(json_file) as open_file: + return json.load(open_file) From d4370f8037994acf350701893cf2e6a52b2588bd Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 10 Oct 2020 13:22:36 -0500 Subject: [PATCH 107/204] Updating Headers to have rate limits --- tests/tooling_for_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index 63b2275..e491d7b 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -24,6 +24,8 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa "httpRequest": { "headers": { "Authorization": [f"Api-Token {cluster.get('api_token').get(tenant)}"], + "x-ratelimit-remaining": 100000000, + "x-ratelimit-limit": 100000000 }, "path": url_path, "method": request_type From c8a915735174e99efcd74b836bd9182a1c1ecaa7 Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 10 Oct 2020 13:26:08 -0500 Subject: [PATCH 108/204] Fixing Rate Limiter --- tests/tooling_for_test.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index e491d7b..2e8743f 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -23,9 +23,7 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa expectation = { "httpRequest": { "headers": { - "Authorization": [f"Api-Token {cluster.get('api_token').get(tenant)}"], - "x-ratelimit-remaining": 100000000, - "x-ratelimit-limit": 100000000 + "Authorization": [f"Api-Token {cluster.get('api_token').get(tenant)}"] }, "path": url_path, "method": request_type @@ -62,7 +60,9 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa "json": response_payload, } expectation["httpResponse"]["headers"] = { - "content-type": ["application/json"] + "content-type": ["application/json"], + "x-ratelimit-remaining": 100000000, + "x-ratelimit-limit": 100000000 } if "response_code" in kwargs: From 540d2bc3ca201ee26ea52b759e8215878ae6a19f Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 10 Oct 2020 13:29:36 -0500 Subject: [PATCH 109/204] Trying Again --- tests/tooling_for_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index 2e8743f..6d98395 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -61,8 +61,8 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa } expectation["httpResponse"]["headers"] = { "content-type": ["application/json"], - "x-ratelimit-remaining": 100000000, - "x-ratelimit-limit": 100000000 + "x-ratelimit-remaining": '100000000', + "x-ratelimit-limit": '100000000' } if "response_code" in kwargs: From 23ee80a2225bee4cf30c75516d151c027234d4c5 Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 10 Oct 2020 13:33:10 -0500 Subject: [PATCH 110/204] Another Try --- tests/tooling_for_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index 6d98395..0786e3d 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -61,8 +61,8 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa } expectation["httpResponse"]["headers"] = { "content-type": ["application/json"], - "x-ratelimit-remaining": '100000000', - "x-ratelimit-limit": '100000000' + "x-ratelimit-remaining": ['100000000'], + "x-ratelimit-limit": ['100000000'] } if "response_code" in kwargs: From e18cd0b54f79b23f6c2979e68d291bef55eae309 Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 10 Oct 2020 13:50:27 -0500 Subject: [PATCH 111/204] Checking if header exists before checking --- dynatrace/requests/request_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/requests/request_handler.py b/dynatrace/requests/request_handler.py index 01a4501..6b234ef 100644 --- a/dynatrace/requests/request_handler.py +++ b/dynatrace/requests/request_handler.py @@ -101,7 +101,7 @@ def wrapper(*args, **kwargs): cluster = args[0] # Only slow-down SaaS - if not cluster.get('is_managed'): + if not cluster.get('is_managed') and 'x-ratelimit-remaining' in response.headers: # Standard Dynatrace response headers req_remaining = int(response.headers.get('x-ratelimit-remaining')) req_limit = int(response.headers.get('x-ratelimit-limit')) From 5ced748f1540f6cfcaf048b92437f7e35b4c6f4c Mon Sep 17 00:00:00 2001 From: Philipose Date: Sat, 10 Oct 2020 14:05:18 -0500 Subject: [PATCH 112/204] PAF-55 #Added most validators being deteched --- .github/workflows/linter.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index f35b3d6..ef2be6c 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -55,6 +55,11 @@ jobs: LOG_FILE: super-linter.log OUTPUT_FOLDER: super-linter.report OUTPUT_FORMAT: tap + VALIDATE_YAML: true + VALIDATE_JSON: true + VALIDATE_EDITORCONFIG: true + VALIDATE_MARKDOWN: true + VALIDATE_PYTHON_PYLINT: true - run: ls ######################## # Retreive tap reports # From 542da9346b7c9ee5e5539a48ff1ecb08b8bc89d5 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 23 Oct 2020 05:26:57 +0100 Subject: [PATCH 113/204] must pass a dictionary for ** to unpack. can't be None --- dynatrace/framework/request_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index d1db6bb..90453c3 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -220,7 +220,7 @@ def v2_get_results_whole(cluster, tenant, endpoint, item, **kwargs): cursor=cursor, item=item, # OneAgents API requires query params stay the same - **kwargs if endpoint == TenantAPIs.ONEAGENTS else None + **kwargs if endpoint == TenantAPIs.ONEAGENTS else dict(kwargs=None) )) return response From 1d59f76d1149ffe1e9984d2d00f2caddee27c326 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 5 Nov 2020 17:02:45 +0000 Subject: [PATCH 114/204] metrics functionality added --- dynatrace/tenant/metrics.py | 95 +++++++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 15 deletions(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index 0687285..14ba32a 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -1,25 +1,90 @@ """Module for interacting with the Metrics API""" from dynatrace.framework import request_handler as rh -ENDPOINT = rh.TenantAPIs.METRICS +ENDPOINT = str(rh.TenantAPIs.METRICS) -def get_metrics(cluster, tenant, params=None): - """Gets the list of metrics and their details""" - next_page_key = 1 - metrics = [] +def get_metric_descriptor(cluster, tenant, **kwargs): + """Get a list of metric descriptors and their details. + Valid metricSelector must be provided in kwargs. List contains all default + details or anything specified through 'fields' kwarg. + \n + @param cluster (dict) - Dynatrace cluster (as taken from variable set) + @param tenant (str) - name of Dynatrace tenant (as taken from variable set) + \n + @returns list - list of metric descriptors mathing the metricSelector + """ + descriptors = rh.v2_get_results_whole( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT, + item='metrics', + **kwargs + ).get('metrics') - while next_page_key: + return descriptors + + +def get_metric_count(cluster, tenant, **kwargs): + """Get the number of metrics matching the metricSeletor + \n + @param cluster (dict) - Dynatrace cluster (as taken from variable set) + @param tenant (str) - name of Dynatrace tenant (as taken from variable set) + \n + @returns int - Number of metrics matching the metricSelector + """ + count = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT, + params=kwargs + ).json().get('totalCount') + + return count + + +def get_metric_data(cluster, tenant, **kwargs): + """Gets data points for given metrics. + One or more metrics and aggregations can be specified using a metricSelector. + The function grabs the datapoints for all entities matching entitySelector if + this was specified. Results are indexed in a dictionary with the metric_id as + key and the data as a list. + \n + @param cluster (dict) - Dynatrace cluster (as taken from variable set) + @param tenant (str) - name of Dynatrace tenant (as taken from variable set) + \n + @kwargs metricSelector (str) - mandatory. used to pass in ID of queried metri(s) + \n + @returns dict - metric data as dictionary with metric id as key + \n + @throws Exception - exception as thrown from downstream + """ + nextPageKey = 1 + results = {} + + while nextPageKey: # Upon subsequent calls, clear all other params - if next_page_key != 1: - params = dict(nextPageKey=next_page_key) + if nextPageKey != 1: + kwargs = dict(nextPageKey=nextPageKey) - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=ENDPOINT, - params=params) + try: + response = rh.make_api_call(cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/query", + params=kwargs) + except Exception as err: + if 'metric key that could not be resolved in the metric registry' in str(err): + break + else: + raise Exception(err) + else: + for result in response.json().get('result'): + metric = result.get('metricId') + if results.get(metric): + results[metric].extend(result.get('data')) + else: + results[metric] = result.get('data') - metrics.extend(response.json().get('metrics')) - next_page_key = response.json().get('nextPageKey') + nextPageKey = response.json().get('nextPageKey') - return metrics + return results From c67136e9557fb28bb17108a36231de39d2f266b7 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 18 Nov 2020 06:51:09 +0000 Subject: [PATCH 115/204] extra functionality for metric api --- dynatrace/tenant/metrics.py | 66 ++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index 14ba32a..383f833 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -53,7 +53,7 @@ def get_metric_data(cluster, tenant, **kwargs): @param cluster (dict) - Dynatrace cluster (as taken from variable set) @param tenant (str) - name of Dynatrace tenant (as taken from variable set) \n - @kwargs metricSelector (str) - mandatory. used to pass in ID of queried metri(s) + @kwargs metricSelector (str) - mandatory. used to pass in ID of queried metric(s) \n @returns dict - metric data as dictionary with metric id as key \n @@ -88,3 +88,67 @@ def get_metric_data(cluster, tenant, **kwargs): nextPageKey = response.json().get('nextPageKey') return results + + +def get_metric_dimension_count(cluster, tenant, metricSelector): + """Function returns the sum total of dimensions defined for one or more metrics. + Useful in DDU calculations for estimating the max number of DDUs that will be + consumed. + + \n + @param cluster (dict) - Dynatrace cluster (as taken from variable set) + @param tenant (str) - name of Dynatrace tenant (as taken from variable set) + @param metricSelector (str) - mandatory. used to pass in ID of queried metric(s) + \n + @returns int - the sum total of dimensions across all matched metrics + """ + details = get_metric_descriptor( + cluster=cluster, + tenant=tenant, + metricSelector=metricSelector, + fields='dimensionDefinitions', + pageSize=5000 + ) + + dimensions = sum( + [len(detail.get('dimensionDefinitions')) + for detail in details] + ) if details else 0 + + return dimensions + + +def get_metric_estimated_ddus(cluster, tenant, metricSelector): + """Function returns the total maximum yearly DDUs that the metrics are allowed + to consume. This is calculated by multiplying the total number of dimensions + by 525.6 (yearly DDUs for 1 metric). This assumes the metric is collected every + minute. Useful for understanding DDU budget requirements. + \n + @param cluster (dict) - Dynatrace cluster (as taken from variable set) + @param tenant (str) - name of Dynatrace tenant (as taken from variable set) + @param metricSelector (str) - mandatory. used to pass in ID of queried metric(s) + \n + @returns (float) - total number of yearly DDUs + """ + return get_metric_dimension_count( + cluster=cluster, + tenant=tenant, + metricSelector=metricSelector + ) * 525.6 + + +# TODO: Refactor make_api_call (PAF-48) +# Payload data must be plain text, not serialised JSON like make_api_call require it. +# Before this functionality can be implemented we must refactor make_api_call to +# use any **kwargs that are valid for the requests module. +# +# def ingest_metrics(cluster, tenant, payload): +# r = rh.make_api_call( +# cluster=cluster, +# tenant=tenant, +# endpoint=f"{ENDPOINT}/ingest", +# json=payload, +# method=rh.HTTP.POST +# ) +# +# return r From b862e45da820974b3d9fc7b133f79537ca5715fa Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 18 Nov 2020 07:32:06 +0000 Subject: [PATCH 116/204] test suite for metrics.py --- .../responses/metrics/datapoints.json | 22 +++ .../responses/metrics/descriptors.json | 42 ++++++ tests/test_metrics.py | 131 ++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 tests/mockserver_payloads/responses/metrics/datapoints.json create mode 100644 tests/mockserver_payloads/responses/metrics/descriptors.json create mode 100644 tests/test_metrics.py diff --git a/tests/mockserver_payloads/responses/metrics/datapoints.json b/tests/mockserver_payloads/responses/metrics/datapoints.json new file mode 100644 index 0000000..17106ba --- /dev/null +++ b/tests/mockserver_payloads/responses/metrics/datapoints.json @@ -0,0 +1,22 @@ +{ + "totalCount": 1, + "nextPageKey": null, + "result": [ + { + "metricId": "builtin:host.mem.avail.pct", + "data": [ + { + "dimensions": [ + "HOST-ABC123DEF456GHI7" + ], + "timestamps": [ + 1605684060000 + ], + "values": [ + 29.36756863040855 + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/metrics/descriptors.json b/tests/mockserver_payloads/responses/metrics/descriptors.json new file mode 100644 index 0000000..8de223f --- /dev/null +++ b/tests/mockserver_payloads/responses/metrics/descriptors.json @@ -0,0 +1,42 @@ +{ + "totalCount": 3, + "nextPageKey": null, + "metrics": [ + { + "metricId": "builtin:host.mem.avail.bytes", + "dimensionDefinitions": [ + { + "key": "dt.entity.host", + "name": "Host", + "displayName": "Host", + "index": 0, + "type": "ENTITY" + } + ] + }, + { + "metricId": "builtin:host.mem.avail.pct", + "dimensionDefinitions": [ + { + "key": "dt.entity.host", + "name": "Host", + "displayName": "Host", + "index": 0, + "type": "ENTITY" + } + ] + }, + { + "metricId": "builtin:host.mem.avail.pfps", + "dimensionDefinitions": [ + { + "key": "dt.entity.host", + "name": "Host", + "displayName": "Host", + "index": 0, + "type": "ENTITY" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/test_metrics.py b/tests/test_metrics.py new file mode 100644 index 0000000..797d19d --- /dev/null +++ b/tests/test_metrics.py @@ -0,0 +1,131 @@ +""" +Test Suite for Metrics API +""" +import unittest +# from user_variables import FULL_SET # pylint: disable=import-error +from user_variables import FULL_SET +from tests import tooling_for_test as testtools +from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.tenant import metrics + +CLUSTER = FULL_SET["mockserver1"] +TENANT = "tenant1" +URL_PATH = str(TenantAPIs.METRICS) +REQUEST_DIR = "tests/mockserver_payloads/requests/metrics" +RESPONSE_DIR = "tests/mockserver_payloads/responses/metrics" + + +class TestGetMetrics(unittest.TestCase): + """Tests cases for fetching metrics and their details.""" + + def test_get_metric_descriptor(self): + """Test fetching descriptors for metrics matching selector.""" + response_file = f"{RESPONSE_DIR}/descriptors.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + parameters={ + "metricSelector": "builtin:host.mem.avail.*" + }, + response_file=response_file + ) + + result = metrics.get_metric_descriptor( + CLUSTER, TENANT, **{'metricSelector': 'builtin:host.mem.avail.*'} + ) + expected_result = testtools.expected_payload(response_file).get('metrics') + self.assertEqual(result, expected_result) + + def test_get_metric_count(self): + """Test fetching the count of metrics matching selector.""" + response_file = f"{RESPONSE_DIR}/descriptors.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + parameters={ + "metricSelector": "builtin:host.mem.avail.*" + }, + response_file=response_file + ) + + result = metrics.get_metric_count( + CLUSTER, TENANT, **{'metricSelector': 'builtin:host.mem.avail.*'} + ) + expected_result = testtools.expected_payload(response_file).get('totalCount') + self.assertEqual(result, expected_result) + + def test_get_metric_data(self): + """Test fetching datapoints for metrics matching selector.""" + response_file = f"{RESPONSE_DIR}/datapoints.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/query", + request_type="GET", + parameters={ + "metricSelector": "builtin:host.mem.avail.pct", + "resolution": "Inf" + }, + response_file=response_file + ) + + result = metrics.get_metric_data( + CLUSTER, TENANT, **{'metricSelector': 'builtin:host.mem.avail.pct', + 'resolution': 'Inf'} + ) + data = testtools.expected_payload(response_file).get('result')[0].get('data') + expected_result = {'builtin:host.mem.avail.pct': data} + self.assertEqual(result, expected_result) + + def test_get_metric_dimension_count(self): + """Test fetching dimension count for metrics matching selector.""" + response_file = f"{RESPONSE_DIR}/descriptors.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + parameters={ + "metricSelector": "builtin:host.mem.avail.*" + }, + response_file=response_file + ) + + result = metrics.get_metric_dimension_count( + CLUSTER, TENANT, **{'metricSelector': 'builtin:host.mem.avail.*'} + ) + expected_result = 3 + self.assertEqual(result, expected_result) + + def test_get_metric_ddus(self): + """Test fetching the estimated DDUs consumed by a metric.""" + response_file = f"{RESPONSE_DIR}/descriptors.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type="GET", + parameters={ + "metricSelector": "builtin:host.mem.avail.*" + }, + response_file=response_file + ) + + result = metrics.get_metric_estimated_ddus( + CLUSTER, TENANT, **{'metricSelector': 'builtin:host.mem.avail.*'} + ) + expected_result = 3 * 525.6 + self.assertEqual(result, expected_result) + + +if __name__ == '__main__': + unittest.main() From 8d04a62a98a5e8a6d02280443f048ef830342617 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 18 Nov 2020 07:54:04 +0000 Subject: [PATCH 117/204] PAF-50: reduced complexity for func. get_metric_data --- dynatrace/tenant/metrics.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index 383f833..ef71b39 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -77,15 +77,15 @@ def get_metric_data(cluster, tenant, **kwargs): break else: raise Exception(err) - else: - for result in response.json().get('result'): - metric = result.get('metricId') - if results.get(metric): - results[metric].extend(result.get('data')) - else: - results[metric] = result.get('data') - - nextPageKey = response.json().get('nextPageKey') + + for result in response.json().get('result'): + metric = result.get('metricId') + if results.get(metric): + results[metric].extend(result.get('data')) + else: + results[metric] = result.get('data') + + nextPageKey = response.json().get('nextPageKey') return results From f0d7102e1cafcad87186e1b481e34e4cf68408bd Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 18 Nov 2020 07:57:55 +0000 Subject: [PATCH 118/204] PAF-50: de-smell.. using specific exception --- dynatrace/tenant/metrics.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index ef71b39..728865a 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -1,5 +1,6 @@ """Module for interacting with the Metrics API""" from dynatrace.framework import request_handler as rh +from dynatrace.framework.exceptions import InvalidAPIResponseException ENDPOINT = str(rh.TenantAPIs.METRICS) @@ -72,12 +73,12 @@ def get_metric_data(cluster, tenant, **kwargs): tenant=tenant, endpoint=f"{ENDPOINT}/query", params=kwargs) - except Exception as err: + except InvalidAPIResponseException as err: if 'metric key that could not be resolved in the metric registry' in str(err): break else: - raise Exception(err) - + raise err + for result in response.json().get('result'): metric = result.get('metricId') if results.get(metric): From d84f68d327c2fed20bb6eb83fce2cf3fa94ce1ad Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 18 Nov 2020 08:00:17 +0000 Subject: [PATCH 119/204] PAF-50: de-smell.. pythonic parameter names --- dynatrace/tenant/metrics.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index 728865a..69f5996 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -91,7 +91,7 @@ def get_metric_data(cluster, tenant, **kwargs): return results -def get_metric_dimension_count(cluster, tenant, metricSelector): +def get_metric_dimension_count(cluster, tenant, metric_selector): """Function returns the sum total of dimensions defined for one or more metrics. Useful in DDU calculations for estimating the max number of DDUs that will be consumed. @@ -99,14 +99,14 @@ def get_metric_dimension_count(cluster, tenant, metricSelector): \n @param cluster (dict) - Dynatrace cluster (as taken from variable set) @param tenant (str) - name of Dynatrace tenant (as taken from variable set) - @param metricSelector (str) - mandatory. used to pass in ID of queried metric(s) + @param metric_selector (str) - mandatory. used to pass in ID of queried metric(s) \n @returns int - the sum total of dimensions across all matched metrics """ details = get_metric_descriptor( cluster=cluster, tenant=tenant, - metricSelector=metricSelector, + metricSelector=metric_selector, fields='dimensionDefinitions', pageSize=5000 ) @@ -119,7 +119,7 @@ def get_metric_dimension_count(cluster, tenant, metricSelector): return dimensions -def get_metric_estimated_ddus(cluster, tenant, metricSelector): +def get_metric_estimated_ddus(cluster, tenant, metric_selector): """Function returns the total maximum yearly DDUs that the metrics are allowed to consume. This is calculated by multiplying the total number of dimensions by 525.6 (yearly DDUs for 1 metric). This assumes the metric is collected every @@ -127,14 +127,14 @@ def get_metric_estimated_ddus(cluster, tenant, metricSelector): \n @param cluster (dict) - Dynatrace cluster (as taken from variable set) @param tenant (str) - name of Dynatrace tenant (as taken from variable set) - @param metricSelector (str) - mandatory. used to pass in ID of queried metric(s) + @param metric_selector (str) - mandatory. used to pass in ID of queried metric(s) \n @returns (float) - total number of yearly DDUs """ return get_metric_dimension_count( cluster=cluster, tenant=tenant, - metricSelector=metricSelector + metricSelector=metric_selector ) * 525.6 From f3f7e7a9c6014a22cd51e43c7462066ce39e0797 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 18 Nov 2020 08:01:48 +0000 Subject: [PATCH 120/204] PAF-50: de-smell.. pythonic parameter names --- dynatrace/tenant/metrics.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index 69f5996..d2b4b7c 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -60,13 +60,13 @@ def get_metric_data(cluster, tenant, **kwargs): \n @throws Exception - exception as thrown from downstream """ - nextPageKey = 1 + next_page_key = 1 results = {} - while nextPageKey: + while next_page_key: # Upon subsequent calls, clear all other params - if nextPageKey != 1: - kwargs = dict(nextPageKey=nextPageKey) + if next_page_key != 1: + kwargs = dict(nextPageKey=next_page_key) try: response = rh.make_api_call(cluster=cluster, @@ -86,7 +86,7 @@ def get_metric_data(cluster, tenant, **kwargs): else: results[metric] = result.get('data') - nextPageKey = response.json().get('nextPageKey') + next_page_key = response.json().get('nextPageKey') return results From 90f11aad69f968e8301ca3a1edb00a6469ecce39 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 18 Nov 2020 08:04:12 +0000 Subject: [PATCH 121/204] PAF-50: de-smell.. replace repeated string with constant --- tests/test_metrics.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 797d19d..36f7846 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -11,6 +11,7 @@ CLUSTER = FULL_SET["mockserver1"] TENANT = "tenant1" URL_PATH = str(TenantAPIs.METRICS) +METRIC_SELECTOR = 'builtin:host.mem.avail.*' REQUEST_DIR = "tests/mockserver_payloads/requests/metrics" RESPONSE_DIR = "tests/mockserver_payloads/responses/metrics" @@ -28,13 +29,13 @@ def test_get_metric_descriptor(self): url_path=URL_PATH, request_type="GET", parameters={ - "metricSelector": "builtin:host.mem.avail.*" + "metricSelector": METRIC_SELECTOR }, response_file=response_file ) result = metrics.get_metric_descriptor( - CLUSTER, TENANT, **{'metricSelector': 'builtin:host.mem.avail.*'} + CLUSTER, TENANT, **{'metricSelector': METRIC_SELECTOR} ) expected_result = testtools.expected_payload(response_file).get('metrics') self.assertEqual(result, expected_result) @@ -49,13 +50,13 @@ def test_get_metric_count(self): url_path=URL_PATH, request_type="GET", parameters={ - "metricSelector": "builtin:host.mem.avail.*" + "metricSelector": METRIC_SELECTOR }, response_file=response_file ) result = metrics.get_metric_count( - CLUSTER, TENANT, **{'metricSelector': 'builtin:host.mem.avail.*'} + CLUSTER, TENANT, **{'metricSelector': METRIC_SELECTOR} ) expected_result = testtools.expected_payload(response_file).get('totalCount') self.assertEqual(result, expected_result) @@ -94,13 +95,13 @@ def test_get_metric_dimension_count(self): url_path=URL_PATH, request_type="GET", parameters={ - "metricSelector": "builtin:host.mem.avail.*" + "metricSelector": METRIC_SELECTOR }, response_file=response_file ) result = metrics.get_metric_dimension_count( - CLUSTER, TENANT, **{'metricSelector': 'builtin:host.mem.avail.*'} + CLUSTER, TENANT, **{'metricSelector': METRIC_SELECTOR} ) expected_result = 3 self.assertEqual(result, expected_result) @@ -115,13 +116,13 @@ def test_get_metric_ddus(self): url_path=URL_PATH, request_type="GET", parameters={ - "metricSelector": "builtin:host.mem.avail.*" + "metricSelector": METRIC_SELECTOR }, response_file=response_file ) result = metrics.get_metric_estimated_ddus( - CLUSTER, TENANT, **{'metricSelector': 'builtin:host.mem.avail.*'} + CLUSTER, TENANT, **{'metricSelector': METRIC_SELECTOR} ) expected_result = 3 * 525.6 self.assertEqual(result, expected_result) From e3898fbbf68bd84710f248138e491c2bc2148b22 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 18 Nov 2020 08:12:51 +0000 Subject: [PATCH 122/204] PAF-50: de-linted --- dynatrace/tenant/metrics.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index d2b4b7c..f2aa72c 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -76,8 +76,7 @@ def get_metric_data(cluster, tenant, **kwargs): except InvalidAPIResponseException as err: if 'metric key that could not be resolved in the metric registry' in str(err): break - else: - raise err + raise for result in response.json().get('result'): metric = result.get('metricId') From 6af79e6cd3486ed7a1a17a3e0d19ba4ea81e15bd Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 18 Nov 2020 08:19:13 +0000 Subject: [PATCH 123/204] PAF-50: fixed failing tests. --- dynatrace/tenant/metrics.py | 2 +- tests/test_metrics.py | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index f2aa72c..bfa45a1 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -133,7 +133,7 @@ def get_metric_estimated_ddus(cluster, tenant, metric_selector): return get_metric_dimension_count( cluster=cluster, tenant=tenant, - metricSelector=metric_selector + metric_selector=metric_selector ) * 525.6 diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 36f7846..6c803e0 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -100,9 +100,7 @@ def test_get_metric_dimension_count(self): response_file=response_file ) - result = metrics.get_metric_dimension_count( - CLUSTER, TENANT, **{'metricSelector': METRIC_SELECTOR} - ) + result = metrics.get_metric_dimension_count(CLUSTER, TENANT, METRIC_SELECTOR) expected_result = 3 self.assertEqual(result, expected_result) @@ -121,9 +119,7 @@ def test_get_metric_ddus(self): response_file=response_file ) - result = metrics.get_metric_estimated_ddus( - CLUSTER, TENANT, **{'metricSelector': METRIC_SELECTOR} - ) + result = metrics.get_metric_estimated_ddus(CLUSTER, TENANT, METRIC_SELECTOR) expected_result = 3 * 525.6 self.assertEqual(result, expected_result) From bab897171eaf1c1f81837990d021ddda46d7dc87 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 19 Nov 2020 07:10:59 +0000 Subject: [PATCH 124/204] PAF-54: Merged pagination functions for easier use upstream --- dynatrace/framework/request_handler.py | 187 +++++++++---------------- dynatrace/tenant/entities.py | 6 +- dynatrace/tenant/metrics.py | 2 +- dynatrace/tenant/oneagents.py | 4 +- 4 files changed, 75 insertions(+), 124 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 90453c3..a13e23c 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -158,155 +158,106 @@ def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method return response -def __get_v2_multipage_results(cluster, tenant, endpoint, item, cursor, **kwargs): - """ - Private function: not intended for calling from outside of this module. - Retrieves subsequent pages of multi-page API call and gathers just the - items requested through the endpoint (e.g. entities, metrics, etc.) +def get_results_whole(cluster, tenant, endpoint, **kwargs): + """Gets a multi-paged result set and returns it whole. \n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param cursor - cursor that was returned with the first page of results\n - @param item - item being retrieved (e.g. entities, metrics, etc.) - """ - results_full = [] - while cursor: - kwargs['nextPageKey'] = cursor - results_page = make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=endpoint, - params=kwargs - ).json() - - # Collect just the items being queried - results_full.extend(results_page.get(item)) - - # Renew cursor - cursor = results_page.get('nextPageKey') - - return results_full - - -def v2_get_results_whole(cluster, tenant, endpoint, item, **kwargs): - """ - Gets a multi-paged result set and returns it whole. To be used with V2 API - pagination where the nextPageKey is returned in the body of the response. - Also this type of query requires the queried item so we can extract it from - the subsequent pages and omit the summary data. + @param cluster (dict) - Dynatrace cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace tenant (as taken from variable set)\n + @param endpoint (str) - API endpoint to call. Use the TenantAPIs Enum.\n \n - @param item - item being retrieved (e.g. entities, metrics, etc.)\n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param params - dictionary of query string parameters - """ - # Get the first results set (including cursor) - response = make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=endpoint, - params=kwargs - ).json() - - # In the case of multi-page, get the rest - cursor = response.get('nextPageKey') - if cursor: - response[item].extend(__get_v2_multipage_results( - cluster=cluster, - endpoint=endpoint, - tenant=tenant, - cursor=cursor, - item=item, - # OneAgents API requires query params stay the same - **kwargs if endpoint == TenantAPIs.ONEAGENTS else dict(kwargs=None) - )) - return response - - -def v1_get_results_whole(cluster, endpoint, tenant, **kwargs): - """ - Gets a multi-paged result set and returns it whole. To be used with V1 API - pagination where the next-page-key is returned in the response headers. + @kwargs item (str) - the item to be retrieved from results response (e.g. entities)\n \n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @kwargs - dictionary of query string parameters + @throws ValueError - when V2 API is used but no item is given """ - results = [] - # We'll always make at least 1 call + # Ensure it always makes at least 1 call cursor = 1 + # For V2 and OneAgents APIs must specify the item collected + if '/api/v2/' in str(endpoint) or endpoint == TenantAPIs.ONEAGENTS: + v2 = True + if 'item' not in kwargs: + raise ValueError("For V2 APIs you must provide collected item.") + item = kwargs['item'] + results = {} + else: + v2 = False + results = [] + while cursor: if cursor != 1: - kwargs['nextPageKey'] = cursor + if not v2 or endpoint == TenantAPIs.ONEAGENTS: + # V1 and OneAgents require all other query params are preserved + kwargs['nextPageKey'] = cursor + else: + # V2 requires all other query params are removed + kwargs = dict(nextPageKey=cursor) + response = make_api_call( cluster=cluster, tenant=tenant, endpoint=endpoint, params=kwargs ) - results.extend(response.json()) - cursor = response.headers.get('next-page-key') + + # V2 returns additional data in response that should be preserved + if v2: + if cursor == 1: + results = response.json() + else: + results[item].extend(response.json().get(item)) + cursor = response.json().get('nextPageKey') + else: + results.extend(response.json()) + cursor = response.headers.get('next-page-key') return results -def v1_get_results_by_page(cluster, endpoint, tenant, **kwargs): - """ - Gets a multi-paged result set one page at a time. To be used with V1 API - pagination where the next-page-key is returned in the response headers. +def get_results_by_page(cluster, tenant, endpoint, **kwargs): + """Gets a multi-paged result set one page at at time. + Useful for parsing very large result sets (e.g. entities) in optimal manner. \n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param params - dictionary of query string parameters - """ - cursor = 1 - while cursor: - if cursor != 1: - kwargs['nextPageKey'] = cursor - response = make_api_call( - cluster=cluster, - tenant=tenant, - endpoint=endpoint, - params=kwargs - ) - # Pause here and return this page of results - yield response.json() - cursor = response.headers.get('next-page-key') - - -def v2_get_results_by_page(cluster, tenant, endpoint, item, **kwargs): - """ - Gets a multi-paged result set one page at a time. To be used with V2 API - pagination where the nextPageKey is returned in the body of the response. + @param cluster (dict) - Dynatrace cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace tenant (as taken from variable set)\n + @param endpoint (str) - API endpoint to call. Use the TenantAPIs Enum.\n \n - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param params - dictionary of query string parameters + @kwargs item (str) - the item to be retrieved from results response (e.g. entities)\n + \n + @throws ValueError - when V2 API is used but no item is given """ - # We'll always make at least 1 call + # Ensure it always makes at least 1 call cursor = 1 + # Check whether pagination behaviour is for V1 or V2 APIs + if '/api/v2/' in str(endpoint): + v2 = True + if 'item' not in kwargs: + raise ValueError("For v2 APIs you must provide collected item.") + item = kwargs['item'] + else: + v2 = False + while cursor: - # On subsequent calls, must omit all other params (except OneAgents API) if cursor != 1: - if endpoint == TenantAPIs.ONEAGENTS: - kwargs['nextPageKey'] = cursor - else: + # V2 requires all other query params are removed + if v2: kwargs = dict(nextPageKey=cursor) + # V1 requires all other query params are preserved + else: + kwargs['nextPageKey'] = cursor response = make_api_call( cluster=cluster, endpoint=endpoint, tenant=tenant, params=kwargs - ).json() + ) - yield response.get(item) - cursor = response.get('nextPageKey') + # OneAgents API pagination behaves like V1 but results returned are like V2 + if v2 or endpoint == TenantAPIs.ONEAGENTS: + yield response.json().get(item) + cursor = response.json().get('nextPageKey') + else: + yield response.json() + cursor = response.headers.get('next-page-key') def check_response(response): diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index 2acfa27..7b40ce6 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -128,7 +128,7 @@ def get_entities_tenantwide(cluster, tenant, entity_type, **kwargs): else: kwargs['entitySelector'] = f'type({entity_type})' - response = rh.v2_get_results_whole( + response = rh.get_results_whole( cluster=cluster, tenant=tenant, item='entities', @@ -215,7 +215,7 @@ def get_entities_by_page(cluster, tenant, entity_type, **kwargs): else: kwargs['entitySelector'] = f'type({entity_type})' - response = rh.v2_get_results_by_page( + response = rh.get_results_by_page( cluster=cluster, tenant=tenant, endpoint=rh.TenantAPIs.ENTITIES, @@ -246,7 +246,7 @@ def get_entity(cluster, tenant, entity_id, **kwargs): else: kwargs['entitySelector'] = f'entityId({entity_id})' - response = rh.v2_get_results_whole( + response = rh.get_results_whole( cluster=cluster, tenant=tenant, endpoint=rh.TenantAPIs.ENTITIES, diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index bfa45a1..d09d11a 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -15,7 +15,7 @@ def get_metric_descriptor(cluster, tenant, **kwargs): \n @returns list - list of metric descriptors mathing the metricSelector """ - descriptors = rh.v2_get_results_whole( + descriptors = rh.get_results_whole( cluster=cluster, tenant=tenant, endpoint=ENDPOINT, diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index d6bb36c..90f6632 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -13,7 +13,7 @@ def get_host_units_tenantwide(cluster, tenant, **kwargs): """ host_units = 0 - host_list = rh.v1_get_results_whole( + host_list = rh.get_results_whole( cluster=cluster, tenant=tenant, endpoint=f'{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts', @@ -82,7 +82,7 @@ def get_oneagents_tenantwide(cluster, tenant, **kwargs): @returns - list of OneAgents """ - return rh.v2_get_results_whole( + return rh.get_results_whole( cluster=cluster, tenant=tenant, endpoint=rh.TenantAPIs.ONEAGENTS, From 608fc3b7181b33679d66b47a8a3ab92d7f491441 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 19 Nov 2020 07:48:43 +0000 Subject: [PATCH 125/204] PAF-50 (lint): pythonic variable names --- dynatrace/framework/request_handler.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index a13e23c..61ea3d0 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -172,19 +172,19 @@ def get_results_whole(cluster, tenant, endpoint, **kwargs): # Ensure it always makes at least 1 call cursor = 1 # For V2 and OneAgents APIs must specify the item collected - if '/api/v2/' in str(endpoint) or endpoint == TenantAPIs.ONEAGENTS: - v2 = True + if '/api/is_v2/' in str(endpoint) or endpoint == TenantAPIs.ONEAGENTS: + is_v2 = True if 'item' not in kwargs: raise ValueError("For V2 APIs you must provide collected item.") item = kwargs['item'] results = {} else: - v2 = False + is_v2 = False results = [] while cursor: if cursor != 1: - if not v2 or endpoint == TenantAPIs.ONEAGENTS: + if not is_v2 or endpoint == TenantAPIs.ONEAGENTS: # V1 and OneAgents require all other query params are preserved kwargs['nextPageKey'] = cursor else: @@ -199,7 +199,7 @@ def get_results_whole(cluster, tenant, endpoint, **kwargs): ) # V2 returns additional data in response that should be preserved - if v2: + if is_v2: if cursor == 1: results = response.json() else: @@ -227,18 +227,18 @@ def get_results_by_page(cluster, tenant, endpoint, **kwargs): # Ensure it always makes at least 1 call cursor = 1 # Check whether pagination behaviour is for V1 or V2 APIs - if '/api/v2/' in str(endpoint): - v2 = True + if '/api/is_v2/' in str(endpoint): + is_v2 = True if 'item' not in kwargs: - raise ValueError("For v2 APIs you must provide collected item.") + raise ValueError("For is_v2 APIs you must provide collected item.") item = kwargs['item'] else: - v2 = False + is_v2 = False while cursor: if cursor != 1: # V2 requires all other query params are removed - if v2: + if is_v2: kwargs = dict(nextPageKey=cursor) # V1 requires all other query params are preserved else: @@ -252,7 +252,7 @@ def get_results_by_page(cluster, tenant, endpoint, **kwargs): ) # OneAgents API pagination behaves like V1 but results returned are like V2 - if v2 or endpoint == TenantAPIs.ONEAGENTS: + if is_v2 or endpoint == TenantAPIs.ONEAGENTS: yield response.json().get(item) cursor = response.json().get('nextPageKey') else: From 5b66d518129542ead808f8d08412f9c04ce6ff63 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 19 Nov 2020 07:52:52 +0000 Subject: [PATCH 126/204] PAF-50 (lint): over-refactored last item. changed endpoint text match back. --- dynatrace/framework/request_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 61ea3d0..3dbc84f 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -172,7 +172,7 @@ def get_results_whole(cluster, tenant, endpoint, **kwargs): # Ensure it always makes at least 1 call cursor = 1 # For V2 and OneAgents APIs must specify the item collected - if '/api/is_v2/' in str(endpoint) or endpoint == TenantAPIs.ONEAGENTS: + if '/api/v2/' in str(endpoint) or endpoint == TenantAPIs.ONEAGENTS: is_v2 = True if 'item' not in kwargs: raise ValueError("For V2 APIs you must provide collected item.") @@ -227,7 +227,7 @@ def get_results_by_page(cluster, tenant, endpoint, **kwargs): # Ensure it always makes at least 1 call cursor = 1 # Check whether pagination behaviour is for V1 or V2 APIs - if '/api/is_v2/' in str(endpoint): + if '/api/v2/' in str(endpoint): is_v2 = True if 'item' not in kwargs: raise ValueError("For is_v2 APIs you must provide collected item.") From 00a671df019d43b7854ff56351d9f9aeedaa85be Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Thu, 19 Nov 2020 08:23:06 +0000 Subject: [PATCH 127/204] PAF-48: refactored make_api_call to use **kwargs --- dynatrace/framework/request_handler.py | 37 +++++++++++++------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 3dbc84f..f988f41 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -114,27 +114,27 @@ def wrapper(*args, **kwargs): @slow_down -def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method=HTTP.GET): - ''' - Function makes an API call in a safe way, taking into account the rate limits. - This will ensure the API call will always go through.\n - The program will wait for the limit to reset if needed.\n - - @param cluster - Cluster dictionary from variable_set\n - @param endpoint - API endpoint to call.\n - @param tenant - String of tenant name used in cluster dictionary\n - @param json - dictionary to be converted to JSON request\n - @param method - HTTP method to use in call. Use HTTP enum.\n +def make_api_call(cluster, endpoint, tenant=None, method=HTTP.GET, **kwargs): + """Function makes an API call in a safe way. + It takes into account any API rate limits. This will ensure the API call + will always go through. The program will wait for the limit to reset if + needed. \n - @return - response from request\n - ''' + @param cluster (dict) - Cluster dictionary from variable_set\n + @param endpoint (str) - API endpoint to call.\n + @param tenant (str) - String of tenant name used in cluster dictionary\n + @param method (str) - HTTP method to use in call. Use HTTP enum. + \n + @kwargs params (dict) - query string parameters\n + @kwargs json (dict) - request body to be sent as JSON\n + @kwargs body (str) - request body to be sent as plain text + \n + @returns - response from request\n + """ # Set the right URL for the operation url = f"{generate_tenant_url(cluster, tenant)}{endpoint}" \ if tenant else f"{HTTPS_STR}{cluster['url']}{endpoint}" - if not params: - params = {} - # Get correct token for the operation if 'onpremise' in str(endpoint) or 'cluster' in str(endpoint): check_managed(cluster) @@ -147,10 +147,9 @@ def make_api_call(cluster, endpoint, tenant=None, params=None, json=None, method response = requests.request( method=str(method), url=url, - params=params, headers=headers, - json=json, - verify=cluster.get('verify_ssl') + verify=cluster.get('verify_ssl'), + **kwargs ) if check_response(response): break From 228d0e18963a813ff92e7bab52b6606375779965 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 20 Nov 2020 06:31:39 +0000 Subject: [PATCH 128/204] PAF-56: added metric ingest function --- dynatrace/tenant/metrics.py | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index d09d11a..f856708 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -137,18 +137,21 @@ def get_metric_estimated_ddus(cluster, tenant, metric_selector): ) * 525.6 -# TODO: Refactor make_api_call (PAF-48) -# Payload data must be plain text, not serialised JSON like make_api_call require it. -# Before this functionality can be implemented we must refactor make_api_call to -# use any **kwargs that are valid for the requests module. -# -# def ingest_metrics(cluster, tenant, payload): -# r = rh.make_api_call( -# cluster=cluster, -# tenant=tenant, -# endpoint=f"{ENDPOINT}/ingest", -# json=payload, -# method=rh.HTTP.POST -# ) -# -# return r +def ingest_metrics(cluster, tenant, payload): + """Ingests metrics based on given payload. + Payload must be formatted according to Dynatrace line-protocol for metric ingest. + \n + @param cluster (dict) - Dynatrace cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace tenant (as taken from variable set)\n + @param payload (str) - payload for metric ingestion. must be formatted according to + Dynatrace line protocol. + \n + @returns (dict) - response to HTTP request + """ + return rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/ingest", + body=payload, + method=rh.HTTP.POST + ) From f42444815b16f488fc96bc51d82007f0137b1506 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 20 Nov 2020 07:11:30 +0000 Subject: [PATCH 129/204] PAF-56: changed request body to data to comply with requests library --- dynatrace/tenant/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index f856708..50ec5af 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -152,6 +152,6 @@ def ingest_metrics(cluster, tenant, payload): cluster=cluster, tenant=tenant, endpoint=f"{ENDPOINT}/ingest", - body=payload, + data=payload, method=rh.HTTP.POST ) From d6e5b6c610760e4133197846fc6b3c608a7454de Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 20 Nov 2020 07:12:29 +0000 Subject: [PATCH 130/204] PAF-56: added support for plain text request body. improved comments --- tests/tooling_for_test.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index a4ad1d0..8fa3092 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -8,16 +8,20 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwargs): - """Create Payload For MockServer to expect and respond - - Args: - cluster (Dictionary): [description] - tenant (str): [description] - url_path (str): [description] - request_type (HTTP str): [description] - - Raises: - ValueError: [description] + """Creates an expectation for a mockserver request. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - name of Tenant (as taken from variable set)\n + @param url_path (str) - path for the request that matches this expectation\n + @param request_type (HTTP str) - type of HTTP request that matches expectation + \n + @kwargs parameters (dict) - query string parameters for the request\n + @kwargs request_file (str) - path to JSON file representing request payload\n + @kwargs request_data (str) - path to plain-text file representing request payload + @kwargs response_body (str) - path to JSON file representing response to request\n + @kwargs response_code (int) - HTTP response code + \n + @throws ValueError - when the response code is not positive """ requests.packages.urllib3.disable_warnings() # pylint: disable=no-member expectation = { @@ -59,6 +63,15 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa "json": request_payload, } + if "request_data" in kwargs: + with open(kwargs['request_data']) as file: + request_data = file.read() + expectation["httpRequest"]["body"] = { + "type": "STRING", + "string": request_data, + "contentType": "text/plain" + } + if "response_file" in kwargs: with open(kwargs['response_file']) as open_file: response_payload = json.load(open_file) From dc2a9b7b71812756a49e3821e87574160e56d8e3 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 20 Nov 2020 07:13:00 +0000 Subject: [PATCH 131/204] PAF-56: added testing for metrics ingest --- .../requests/metrics/payload.txt | 3 +++ tests/test_metrics.py | 22 +++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/mockserver_payloads/requests/metrics/payload.txt diff --git a/tests/mockserver_payloads/requests/metrics/payload.txt b/tests/mockserver_payloads/requests/metrics/payload.txt new file mode 100644 index 0000000..b1c8306 --- /dev/null +++ b/tests/mockserver_payloads/requests/metrics/payload.txt @@ -0,0 +1,3 @@ +cpu.temperature,cpu=Intel,core=1 gauge,min=20,max=30,sum=50,count=2 +cpu.temperature,cpu=Intel,core=2 gauge,min=10,max=30,sum=40,count=2 +cpu.temperature,cpu=Intel,core=3 gauge,min=30,max=40,sum=70,count=2 \ No newline at end of file diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 6c803e0..2c8dfa7 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -124,5 +124,27 @@ def test_get_metric_ddus(self): self.assertEqual(result, expected_result) +class TestPushMetrics(unittest.TestCase): + """Tests for metrics ingestion capability""" + + def test_metrics_ingest(self): + """Tests simple metric ingestion""" + request_file = f"{REQUEST_DIR}/payload.txt" + with open(file=request_file, mode='r') as f: + payload = f.read() + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/ingest", + request_type="POST", + request_data=request_file, + response_code=202 + ) + + result = metrics.ingest_metrics(CLUSTER, TENANT, payload) + self.assertEqual(result.status_code, 202) + + if __name__ == '__main__': unittest.main() From 468b8edf14a2632e852f1d52db16c42e522026c2 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 20 Nov 2020 07:35:32 +0000 Subject: [PATCH 132/204] PAF-56 (lint): pythonic var names --- tests/test_metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 2c8dfa7..359902f 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -130,8 +130,8 @@ class TestPushMetrics(unittest.TestCase): def test_metrics_ingest(self): """Tests simple metric ingestion""" request_file = f"{REQUEST_DIR}/payload.txt" - with open(file=request_file, mode='r') as f: - payload = f.read() + with open(file=request_file, mode='r') as text: + payload = text.read() testtools.create_mockserver_expectation( cluster=CLUSTER, From 98fc7ce23989f65ec918e16b8d7ab726e92bdbae Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 21 Nov 2020 10:51:13 +0000 Subject: [PATCH 133/204] PAF-57: extensions base functionality --- dynatrace/tenant/extensions.py | 287 ++++++++++++++++++++++++++++++--- 1 file changed, 265 insertions(+), 22 deletions(-) diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index 7ad7e09..45b6424 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -1,35 +1,278 @@ -"""Operations Interacting with Dynatrace Extensions""" +"""Operations Interacting with Dynatrace Extensions API""" from dynatrace.framework import request_handler as rh ENDPOINT = rh.TenantAPIs.EXTENSIONS -def get_all_extensions(cluster, tenant, params=None): - """ Gets the list of all extensions available""" - # TODO: Add pagination +def get_all_extensions(cluster, tenant, pageSize=200): + """Gets a list of all extensions available on the tenant. + List is returned whole regardless of page size; page size can be used to control the + number of API calls. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param pageSize (int) - page size between 1 and 500 (default 200) + \n + @returns list - list of extensions + """ + extension_list = rh.get_results_whole( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT, + api_version=2, + pageSize=pageSize, + item="extensions" + ).get('extensions') - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=ENDPOINT, - params=params) - return response.json().get('extensions') + return extension_list def get_extension_details(cluster, tenant, extension_id): - """ Get the details of a specific extension""" + """Get the details of a specific extension. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to get the details for + \n + @returns (dict) - JSON response containing extension details + """ + details = rh.make_api_call( + cluster=cluster, + endpoint=f"{ENDPOINT}/{extension_id}", + tenant=tenant + ).json() - response = rh.make_api_call(cluster=cluster, - endpoint=f"{ENDPOINT}/{extension_id}", - tenant=tenant) - return response.json() + return details -def get_extension_states(cluster, tenant, extension_id, params=None): - """ Gets all the deployment states of a specific extension""" - # TODO: Add pagination - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=f"{ENDPOINT}/{extension_id}/states", - params=params) +def get_extension_global_config(cluster, tenant, extension_id): + """Gets the global configuration for a given extension. + Does not apply to ActiveGate extensions. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to get the config for + \n + @returns dict - global configuration + """ + config = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{extension_id}/global" + ).json() - return response.json().get('states') + return config + + +def get_extension_instance_config(cluster, tenant, extension_id, instance_id): + """Gets the configuration for an instance of an extension. + For remote extensions this is an endpoint config, otherwise a host config. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to get the config for\n + @param instance_id (str) - ID of extension instance to get config for + \n + @returns dict - instance configuration + """ + config = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{extension_id}/instances/{instance_id}" + ).json() + + return config + + +def get_extension_states(cluster, tenant, extension_id): + """Gets all the deployment states (instances) of a specific extension. + For remote extensions these are endpoints, for other extensions these are + processes/hosts. States are independent of global/instance configuration. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to get the states for + \n + @returns list - states/instances of this extension + """ + states = rh.get_results_whole( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{extension_id}/states", + api_version=2, + item="states" + ).get('states') + + return states + + +def get_extension_instances(cluster, tenant, extension_id): + """Gets all the configuration instances of a specific extension. + An instance is an endpoint for a remote extension, otherwise a host. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to get the states for + \n + @returns list - configuration instances for this extension + """ + instances = rh.get_results_whole( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{extension_id}/instances", + api_version=2, + item="configurationsList" + ).get('configurationsList') + + return instances + + +def get_extension_metrics(cluster, tenant, extension_id): + pass + + +def enable_global_config(cluster, tenant, extension_id): + """Enables the global configuration for an extension. + Not applicable to remote extensions. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to enable + \n + @returns dict - HTTP response for the request + """ + config = get_extension_global_config(cluster, tenant, extension_id) + + config['enabled'] = True + + response = update_global_config(cluster, tenant, extension_id, config) + + return response + + +def disable_global_config(cluster, tenant, extension_id): + """Disables the global configuration for an extension. + Not applicable to remote extensions. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to disable + \n + @returns dict - HTTP response for the request + """ + config = get_extension_global_config(cluster, tenant, extension_id) + + config['enabled'] = False + + response = update_global_config(cluster, tenant, extension_id, config) + + return response + + +def update_global_config(cluster, tenant, extension_id, config): + """Updates the global configuration for an extension. + Not applicable to remote extensions. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to update\n + @param config (dict) - new configuration as JSON dictionary + \n + @returns dict - HTTP response to request + """ + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{extension_id}/global", + method=rh.HTTP.PUT, + json=config + ) + + return response + + +def enable_instance_config(cluster, tenant, extension_id, instance_id): + """Enables the configuration for an instance of an extension. + Instance is endpoint for a remote extension, otherwise a host. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to enable\n + @param instance_id (str) - ID of extension instance to enable + \n + @returns dict - HTTP response to request + """ + config = get_extension_instance_config( + cluster, tenant, extension_id, instance_id + ) + + config['enabled'] = True + + # API BUG: For remote extensions useGlobal is null, but API call doesn't support it + if config.get('useGlobal') is None: + config['useGlobal'] = False + # API BUG: For remote extensions the extension ID in the config is the instance id + # this needs to be set back to the extension ID otherwise fails. + if 'activeGate' in config: + config['extensionId'] = extension_id + + response = update_instance_config( + cluster, tenant, extension_id, instance_id, config + ) + + return response + + +def disable_instance_config(cluster, tenant, extension_id, instance_id): + """Disables the configuration for an instance of an extension. + Instance is endpoint for a remote extension, otherwise a host. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to enable\n + @param instance_id (str) - ID of extension instance to disable + \n + @returns dict - HTTP response to request + """ + config = get_extension_instance_config( + cluster, tenant, extension_id, instance_id + ) + + config['enabled'] = False + + # API BUG: For remote extensions useGlobal is null, but API call doesn't support it + if config.get('useGlobal') is None: + config['useGlobal'] = False + # API BUG: For remote extensions the extension ID in the config is the instance id + # this needs to be set back to the extension ID otherwise fails. + if 'activeGate' in config: + config['extensionId'] = extension_id + + response = update_instance_config( + cluster, tenant, extension_id, instance_id, config + ) + + return response + + +def update_instance_config(cluster, tenant, extension_id, instance_id, config): + """Updates the configuration for an instance of an extension. + Instance is endpoint for a remote extension, otherwise a host. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension to update\n + @param instance_id (str) - ID of extension instance to update + @param config (dict) - new configuration as JSON dictionary + \n + @returns dict - HTTP response to request + """ + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{extension_id}/instances/{instance_id}", + method=rh.HTTP.PUT, + json=config + ) + + return response From ae6b8a1de9c61095caea4e1e2a99c8dfa253adbd Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 21 Nov 2020 10:52:10 +0000 Subject: [PATCH 134/204] PAF-57: added version parameter to pagination to account for exceptions to apis --- dynatrace/framework/request_handler.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index f988f41..14c4b11 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -157,12 +157,14 @@ def make_api_call(cluster, endpoint, tenant=None, method=HTTP.GET, **kwargs): return response -def get_results_whole(cluster, tenant, endpoint, **kwargs): +def get_results_whole(cluster, tenant, endpoint, api_version, **kwargs): """Gets a multi-paged result set and returns it whole. \n @param cluster (dict) - Dynatrace cluster (as taken from variable set)\n @param tenant (str) - name of Dynatrace tenant (as taken from variable set)\n @param endpoint (str) - API endpoint to call. Use the TenantAPIs Enum.\n + @param api_version (int) - different APIs have different pagination behaviour. + this maps the pagination behaviour to v1 or v2. \n @kwargs item (str) - the item to be retrieved from results response (e.g. entities)\n \n @@ -170,8 +172,8 @@ def get_results_whole(cluster, tenant, endpoint, **kwargs): """ # Ensure it always makes at least 1 call cursor = 1 - # For V2 and OneAgents APIs must specify the item collected - if '/api/v2/' in str(endpoint) or endpoint == TenantAPIs.ONEAGENTS: + # For V2 APIs must specify the item collected + if api_version == 2: is_v2 = True if 'item' not in kwargs: raise ValueError("For V2 APIs you must provide collected item.") @@ -203,7 +205,11 @@ def get_results_whole(cluster, tenant, endpoint, **kwargs): results = response.json() else: results[item].extend(response.json().get(item)) - cursor = response.json().get('nextPageKey') + # Extensions API pagination behaves like V2 but token is nextPageToken + if str(endpoint).startswith(str(TenantAPIs.EXTENSIONS)): + cursor = response.json().get('nextPageToken') + else: + cursor = response.json().get('nextPageKey') else: results.extend(response.json()) cursor = response.headers.get('next-page-key') @@ -253,7 +259,11 @@ def get_results_by_page(cluster, tenant, endpoint, **kwargs): # OneAgents API pagination behaves like V1 but results returned are like V2 if is_v2 or endpoint == TenantAPIs.ONEAGENTS: yield response.json().get(item) - cursor = response.json().get('nextPageKey') + # Extensions API pagination behaves like V2 but token is nextPageToken + if endpoint.startswith(str(TenantAPIs.EXTENSIONS)): + cursor = response.json().get('nextPageToken') + else: + cursor = response.json().get('nextPageKey') else: yield response.json() cursor = response.headers.get('next-page-key') From ffc842ef717116f42c8221803e6ba7d53ccb0e32 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 21 Nov 2020 11:26:56 +0000 Subject: [PATCH 135/204] PAF-57: removed metrics calculation for now --- dynatrace/tenant/extensions.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index 45b6424..9ee92a3 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -127,10 +127,6 @@ def get_extension_instances(cluster, tenant, extension_id): return instances -def get_extension_metrics(cluster, tenant, extension_id): - pass - - def enable_global_config(cluster, tenant, extension_id): """Enables the global configuration for an extension. Not applicable to remote extensions. From 260ebc02e41dd5b7a3e943b1f2bddec92cac5489 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 21 Nov 2020 11:56:37 +0000 Subject: [PATCH 136/204] PAF-57: fixed pagination in request handler --- dynatrace/framework/request_handler.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 14c4b11..95478c1 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -205,11 +205,7 @@ def get_results_whole(cluster, tenant, endpoint, api_version, **kwargs): results = response.json() else: results[item].extend(response.json().get(item)) - # Extensions API pagination behaves like V2 but token is nextPageToken - if str(endpoint).startswith(str(TenantAPIs.EXTENSIONS)): - cursor = response.json().get('nextPageToken') - else: - cursor = response.json().get('nextPageKey') + cursor = response.json().get('nextPageKey') else: results.extend(response.json()) cursor = response.headers.get('next-page-key') @@ -217,13 +213,15 @@ def get_results_whole(cluster, tenant, endpoint, api_version, **kwargs): return results -def get_results_by_page(cluster, tenant, endpoint, **kwargs): +def get_results_by_page(cluster, tenant, endpoint, api_version, **kwargs): """Gets a multi-paged result set one page at at time. Useful for parsing very large result sets (e.g. entities) in optimal manner. \n @param cluster (dict) - Dynatrace cluster (as taken from variable set)\n @param tenant (str) - name of Dynatrace tenant (as taken from variable set)\n @param endpoint (str) - API endpoint to call. Use the TenantAPIs Enum.\n + @param api_version (int) - different APIs have different pagination behaviour. + this maps the pagination behaviour to v1 or v2. \n @kwargs item (str) - the item to be retrieved from results response (e.g. entities)\n \n @@ -232,7 +230,7 @@ def get_results_by_page(cluster, tenant, endpoint, **kwargs): # Ensure it always makes at least 1 call cursor = 1 # Check whether pagination behaviour is for V1 or V2 APIs - if '/api/v2/' in str(endpoint): + if api_version == 2: is_v2 = True if 'item' not in kwargs: raise ValueError("For is_v2 APIs you must provide collected item.") @@ -259,11 +257,7 @@ def get_results_by_page(cluster, tenant, endpoint, **kwargs): # OneAgents API pagination behaves like V1 but results returned are like V2 if is_v2 or endpoint == TenantAPIs.ONEAGENTS: yield response.json().get(item) - # Extensions API pagination behaves like V2 but token is nextPageToken - if endpoint.startswith(str(TenantAPIs.EXTENSIONS)): - cursor = response.json().get('nextPageToken') - else: - cursor = response.json().get('nextPageKey') + cursor = response.json().get('nextPageKey') else: yield response.json() cursor = response.headers.get('next-page-key') From fbe1215e27d1c1aeabf9818c18784b7bba1de804 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 21 Nov 2020 16:58:19 +0000 Subject: [PATCH 137/204] PAF-57: test suite for extensions API --- .../requests/extensions/config.json | 6 + .../responses/extensions/config.json | 6 + .../responses/extensions/details.json | 14 ++ .../responses/extensions/get_all.json | 20 ++ .../responses/extensions/instances.json | 8 + .../responses/extensions/states.json | 35 ++++ tests/test_extensions.py | 177 ++++++++++++++++++ 7 files changed, 266 insertions(+) create mode 100644 tests/mockserver_payloads/requests/extensions/config.json create mode 100644 tests/mockserver_payloads/responses/extensions/config.json create mode 100644 tests/mockserver_payloads/responses/extensions/details.json create mode 100644 tests/mockserver_payloads/responses/extensions/get_all.json create mode 100644 tests/mockserver_payloads/responses/extensions/instances.json create mode 100644 tests/mockserver_payloads/responses/extensions/states.json create mode 100644 tests/test_extensions.py diff --git a/tests/mockserver_payloads/requests/extensions/config.json b/tests/mockserver_payloads/requests/extensions/config.json new file mode 100644 index 0000000..6088e73 --- /dev/null +++ b/tests/mockserver_payloads/requests/extensions/config.json @@ -0,0 +1,6 @@ +{ + "extensionId": "custom.jmx.radujmx123456789", + "enabled": true, + "infraOnlyEnabled": false, + "properties": {} +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/extensions/config.json b/tests/mockserver_payloads/responses/extensions/config.json new file mode 100644 index 0000000..6088e73 --- /dev/null +++ b/tests/mockserver_payloads/responses/extensions/config.json @@ -0,0 +1,6 @@ +{ + "extensionId": "custom.jmx.radujmx123456789", + "enabled": true, + "infraOnlyEnabled": false, + "properties": {} +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/extensions/details.json b/tests/mockserver_payloads/responses/extensions/details.json new file mode 100644 index 0000000..ca07917 --- /dev/null +++ b/tests/mockserver_payloads/responses/extensions/details.json @@ -0,0 +1,14 @@ +{ + "id": "custom.jmx.radujmx123456789", + "name": "radujmx", + "version": "1.123456789", + "type": "JMX", + "metricGroup": "custom.jmx.radujmx", + "metadata": { + "configurationVersions": [ + 0 + ], + "clusterVersion": "1.207" + }, + "properties": [] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/extensions/get_all.json b/tests/mockserver_payloads/responses/extensions/get_all.json new file mode 100644 index 0000000..ac86295 --- /dev/null +++ b/tests/mockserver_payloads/responses/extensions/get_all.json @@ -0,0 +1,20 @@ +{ + "extensions": [ + { + "id": "custom.jmx.radujmx123456789", + "name": "radujmx", + "type": "JMX" + }, + { + "id": "custom.python.oneagent_plugin", + "name": "OneAgent Plugin", + "type": "ONEAGENT" + }, + { + "id": "custom.remote.python.activegate_plugin", + "name": "ActiveGate Plugin", + "type": "ACTIVEGATE" + } + ], + "totalResults": 3 +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/extensions/instances.json b/tests/mockserver_payloads/responses/extensions/instances.json new file mode 100644 index 0000000..05f2510 --- /dev/null +++ b/tests/mockserver_payloads/responses/extensions/instances.json @@ -0,0 +1,8 @@ +{ + "configurationsList": [ + { + "id": "HOST-123ABC456GHI789J" + } + ], + "totalResults": 1 +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/extensions/states.json b/tests/mockserver_payloads/responses/extensions/states.json new file mode 100644 index 0000000..4ed3395 --- /dev/null +++ b/tests/mockserver_payloads/responses/extensions/states.json @@ -0,0 +1,35 @@ +{ + "states": [ + { + "extensionId": "custom.jmx.radujmx123456789", + "version": null, + "endpointId": null, + "state": "OK", + "stateDescription": "", + "timestamp": 1605975229340, + "hostId": "HOST-1111111111111", + "processId": "PROCESS_GROUP_INSTANCE-1111111111111" + }, + { + "extensionId": "custom.jmx.radujmx123456789", + "version": null, + "endpointId": null, + "state": "OK", + "stateDescription": "", + "timestamp": 1605975229340, + "hostId": "HOST-222222222222222", + "processId": "PROCESS_GROUP_INSTANCE-222222222222222" + }, + { + "extensionId": "custom.jmx.radujmx123456789", + "version": null, + "endpointId": null, + "state": "OK", + "stateDescription": "", + "timestamp": 1605975229340, + "hostId": "HOST-333333333333", + "processId": "PROCESS_GROUP_INSTANCE-3333333333333" + } + ], + "totalResults": 3 +} \ No newline at end of file diff --git a/tests/test_extensions.py b/tests/test_extensions.py new file mode 100644 index 0000000..4aef14b --- /dev/null +++ b/tests/test_extensions.py @@ -0,0 +1,177 @@ +"""Test Suite for the Extensions API""" +import unittest +import json +from user_variables import FULL_SET # pylint: disable=import-error +from tests import tooling_for_test as testtools +from dynatrace.framework.request_handler import TenantAPIs, HTTP +from dynatrace.tenant import extensions + +CLUSTER = FULL_SET["mockserver1"] +TENANT = "tenant1" +URL_PATH = str(TenantAPIs.EXTENSIONS) +EXTENSION_ID = "custom.jmx.radujmx123456789" +INSTANCE_ID = "HOST-ABC123DEF456GHI7" +REQUEST_DIR = "tests/mockserver_payloads/requests/extensions" +RESPONSE_DIR = "tests/mockserver_payloads/responses/extensions" + + +class TestGetExtensions(unittest.TestCase): + """Test cases for fetching extensions and their details""" + + def test_get_all_extensions(self): + """Test fetching a list of extensions from a tenant""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = extensions.get_all_extensions(CLUSTER, TENANT) + expected_result = testtools.expected_payload(response_file).get('extensions') + + self.assertEqual(result, expected_result) + + def test_get_extension_details(self): + """Test fetching the details of an extension""" + response_file = f"{RESPONSE_DIR}/details.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{EXTENSION_ID}", + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = extensions.get_extension_details(CLUSTER, TENANT, EXTENSION_ID) + expected_result = testtools.expected_payload(response_file) + + self.assertEqual(result, expected_result) + + def test_get_extension_instances(self): + """Test fetching the list of instances for an extension""" + response_file = f"{RESPONSE_DIR}/instances.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{EXTENSION_ID}/instances", + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = extensions.get_extension_instances(CLUSTER, TENANT, EXTENSION_ID) + expected_result = testtools.expected_payload( + response_file + ).get('configurationsList') + + self.assertEqual(result, expected_result) + + def test_get_extension_states(self): + """Test fetching the list of states for an extension""" + response_file = f"{RESPONSE_DIR}/states.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{EXTENSION_ID}/states", + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = extensions.get_extension_states(CLUSTER, TENANT, EXTENSION_ID) + expected_result = testtools.expected_payload(response_file).get('states') + + self.assertEqual(result, expected_result) + + def test_get_extension_global_config(self): + """Test fetching the global configuration of an extension""" + response_file = f"{RESPONSE_DIR}/config.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{EXTENSION_ID}/global", + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = extensions.get_extension_global_config(CLUSTER, TENANT, EXTENSION_ID) + expected_result = testtools.expected_payload(response_file) + + self.assertEqual(result, expected_result) + + def test_get_extension_instance_config(self): + """Test fetching a configuration instance for an extension""" + response_file = f"{RESPONSE_DIR}/config.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{EXTENSION_ID}/instances/{INSTANCE_ID}", + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = extensions.get_extension_instance_config( + CLUSTER, TENANT, EXTENSION_ID, INSTANCE_ID + ) + expected_result = testtools.expected_payload(response_file) + + self.assertEqual(result, expected_result) + + +class TestModifyExtensions(unittest.TestCase): + """Test cases for modifying extension states and details""" + + def test_update_global_config(self): + """Test updating the global config for an extension""" + request_file = f"{REQUEST_DIR}/config.json" + with open(request_file, "r") as config_file: + config = json.loads(config_file.read()) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{EXTENSION_ID}/global", + request_type=str(HTTP.PUT), + request_file=request_file, + response_code=202 + ) + + result = extensions.update_global_config( + CLUSTER, TENANT, EXTENSION_ID, config + ).status_code + expected_result = 202 + + self.assertEqual(result, expected_result) + + def test_update_instance_config(self): + """Test updating an instance of configuration for an extension""" + request_file = f"{REQUEST_DIR}/config.json" + with open(request_file, "r") as config_file: + config = json.loads(config_file.read()) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{EXTENSION_ID}/instances/{INSTANCE_ID}", + request_type=str(HTTP.PUT), + request_file=request_file, + response_code=202 + ) + + result = extensions.update_instance_config( + CLUSTER, TENANT, EXTENSION_ID, INSTANCE_ID, config + ).status_code + expected_result = 202 + + self.assertEqual(result, expected_result) + + +if __name__ == "__main__": + unittest.main() From f9f3a85d8080ad952c8130f0466d93f22ec7aa93 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 21 Nov 2020 17:09:47 +0000 Subject: [PATCH 138/204] PAF=57: refactored classes to use new pagination system --- dynatrace/tenant/entities.py | 3 +++ dynatrace/tenant/metrics.py | 1 + dynatrace/tenant/oneagents.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index 7b40ce6..949e089 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -131,6 +131,7 @@ def get_entities_tenantwide(cluster, tenant, entity_type, **kwargs): response = rh.get_results_whole( cluster=cluster, tenant=tenant, + api_version=2, item='entities', endpoint=rh.TenantAPIs.ENTITIES, **kwargs @@ -219,6 +220,7 @@ def get_entities_by_page(cluster, tenant, entity_type, **kwargs): cluster=cluster, tenant=tenant, endpoint=rh.TenantAPIs.ENTITIES, + api_version=2, item='entities', **kwargs ) @@ -250,6 +252,7 @@ def get_entity(cluster, tenant, entity_id, **kwargs): cluster=cluster, tenant=tenant, endpoint=rh.TenantAPIs.ENTITIES, + api_version=2, item='entities', **kwargs ) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index 50ec5af..f7bc92a 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -19,6 +19,7 @@ def get_metric_descriptor(cluster, tenant, **kwargs): cluster=cluster, tenant=tenant, endpoint=ENDPOINT, + api_version=2, item='metrics', **kwargs ).get('metrics') diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index 90f6632..bac4643 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -17,6 +17,7 @@ def get_host_units_tenantwide(cluster, tenant, **kwargs): cluster=cluster, tenant=tenant, endpoint=f'{rh.TenantAPIs.V1_TOPOLOGY}/infrastructure/hosts', + api_version=1, **kwargs ) @@ -86,6 +87,7 @@ def get_oneagents_tenantwide(cluster, tenant, **kwargs): cluster=cluster, tenant=tenant, endpoint=rh.TenantAPIs.ONEAGENTS, + api_version=2, item='hosts', **kwargs ).get('hosts') From 9262d204464bfd92db7ae5dda9b8f8fceb198d0c Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 21 Nov 2020 17:11:24 +0000 Subject: [PATCH 139/204] PAF-57 (smell): pythonic variable names --- dynatrace/tenant/extensions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index 9ee92a3..f427d8a 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -4,14 +4,14 @@ ENDPOINT = rh.TenantAPIs.EXTENSIONS -def get_all_extensions(cluster, tenant, pageSize=200): +def get_all_extensions(cluster, tenant, page_size=200): """Gets a list of all extensions available on the tenant. List is returned whole regardless of page size; page size can be used to control the number of API calls. \n @param cluster (dict) - Cluster dictionary (as taken from variable set)\n @param tenant (str) - Tenant name (as taken from variable set)\n - @param pageSize (int) - page size between 1 and 500 (default 200) + @param page_size (int) - page size between 1 and 500 (default 200) \n @returns list - list of extensions """ @@ -20,7 +20,7 @@ def get_all_extensions(cluster, tenant, pageSize=200): tenant=tenant, endpoint=ENDPOINT, api_version=2, - pageSize=pageSize, + pageSize=page_size, item="extensions" ).get('extensions') From 386af88fc6762230027febc0a2e4313b5d9e2bec Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 21 Nov 2020 17:31:06 +0000 Subject: [PATCH 140/204] PAF-57: extension metrics functionality added. --- dynatrace/tenant/extensions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index f427d8a..e293074 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -1,5 +1,6 @@ """Operations Interacting with Dynatrace Extensions API""" from dynatrace.framework import request_handler as rh +from dynatrace.tenant import metrics ENDPOINT = rh.TenantAPIs.EXTENSIONS @@ -45,6 +46,25 @@ def get_extension_details(cluster, tenant, extension_id): return details +def get_extension_metrics(cluster, tenant, extension_id): + """Gets a list of metric IDs that are collected by the extension. + \n + @param cluster (dict) - Cluster dictionary (as taken from variable set)\n + @param tenant (str) - Tenant name (as taken from variable set)\n + @param extension_id (str) - ID of extension + \n + @returns list - list of metric IDs + """ + metric_group = get_extension_details(cluster, tenant, extension_id).get('metricGroup') + ext_metrics = metrics.get_metric_descriptor( + cluster=cluster, + tenant=tenant, + metricSelector=f"ext:{metric_group}.*" + ) + + return list(m.get('metricId') for m in ext_metrics) + + def get_extension_global_config(cluster, tenant, extension_id): """Gets the global configuration for a given extension. Does not apply to ActiveGate extensions. From 0c7e7443345607fef4fcbfba745f334f01fed0a4 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 21 Nov 2020 17:51:36 +0000 Subject: [PATCH 141/204] PAF-57: tests for extension metrics added to suite --- .../responses/extensions/metrics.json | 14 ++++++++ tests/test_extensions.py | 33 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/mockserver_payloads/responses/extensions/metrics.json diff --git a/tests/mockserver_payloads/responses/extensions/metrics.json b/tests/mockserver_payloads/responses/extensions/metrics.json new file mode 100644 index 0000000..a3d4ba4 --- /dev/null +++ b/tests/mockserver_payloads/responses/extensions/metrics.json @@ -0,0 +1,14 @@ +{ + "nextPageKey": null, + "metrics": [ + { + "metricId": "ext:custom.jmx.radujmx.metric_test-count" + }, + { + "metricId": "ext:custom.jmx.radujmx.metric_test-avg" + }, + { + "metricId": "ext:custom.jmx.radujmx.metric_test-sum" + } + ] +} \ No newline at end of file diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 4aef14b..25de504 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -124,6 +124,39 @@ def test_get_extension_instance_config(self): self.assertEqual(result, expected_result) + def test_get_extension_metrics(self): + """Tests fetching the metrics collected by an extension""" + details_file = f"{RESPONSE_DIR}/details.json" + response_file = f"{RESPONSE_DIR}/metrics.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{EXTENSION_ID}", + request_type=str(HTTP.GET), + response_file=details_file, + mock_id="first" + ) + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=str(TenantAPIs.METRICS), + parameters={ + "metricSelector": "ext:custom.jmx.radujmx.*" + }, + mock_id="second", + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = extensions.get_extension_metrics(CLUSTER, TENANT, EXTENSION_ID) + expected_result = list( + m.get('metricId') + for m in testtools.expected_payload(response_file).get('metrics') + ) + + self.assertEqual(result, expected_result) + class TestModifyExtensions(unittest.TestCase): """Test cases for modifying extension states and details""" From 6f8068612d31aeefc8cc7129b7b853c82fa95ac2 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 27 Nov 2020 07:07:33 +0000 Subject: [PATCH 142/204] PAF-59: added new endpoint for problems v2 --- dynatrace/framework/request_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 95478c1..6de4379 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -38,6 +38,7 @@ class TenantAPIs(Enum): PROBLEM_DETAILS = "/api/v1/problem/details" PROBLEM_FEED = "/api/v1/problem/feed" PROBLEM_STATUS = "/api/v1/problem/status" + PROBLEMS = "/api/v2/problems" DEPLOY_ONEAGENT = "/api/v1/deployment/installer/agent" DEPLOY_ONEAGENT_CONNECTION_INFO = "/api/v1/deployment/installer/agent/connectioninfo" DEPLOY_ONEAGENT_CONNECTION_ENDPOINTS = \ From f1c50f708a61fddd73630c33d7799935df041cfa Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 27 Nov 2020 07:08:42 +0000 Subject: [PATCH 143/204] PAF-59: base functionality for Problems API --- dynatrace/tenant/problems.py | 205 +++++++++++++++++++++++++++++++++++ 1 file changed, 205 insertions(+) create mode 100644 dynatrace/tenant/problems.py diff --git a/dynatrace/tenant/problems.py b/dynatrace/tenant/problems.py new file mode 100644 index 0000000..dfe92d6 --- /dev/null +++ b/dynatrace/tenant/problems.py @@ -0,0 +1,205 @@ +"""Module for interactions with the Problems (V2) API""" +from dynatrace.framework import request_handler as rh + +ENDPOINT = str(rh.TenantAPIs.PROBLEMS) + + +def get_all_problems(cluster, tenant, **kwargs): + """Gets the list of all problems mathing the query parameters. + \n + @param cluster (dict) - Dynatrace Cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace Tenant (as taken from variable set) + \n + @returns list - list of problems + """ + problems_list = rh.get_results_whole( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT, + api_version=2, + item="problems", + **kwargs + ).get('problems') + + return problems_list + + +def get_problem_count(cluster, tenant, **kwargs): + """Gets the total number of problems matching query parameters. + \n + @param cluster (dict) - Dynatrace Cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace Tenant (as taken from variable set) + \n + @returns int - number of problems + """ + problems_list = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT, + params=kwargs + ).json() + count = problems_list.get('totalCount') + + return count + + +def get_problem_details(cluster, tenant, problem_id, **kwargs): + """Retrieves the details of a specific problem. + \n + @param cluster (dict) - Dynatrace Cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace Tenant (as taken from variable set)\n + @param problem_id (str) - ID of the problem to retrieve + \n + @kwargs fields (str) - comma separated list of fields to include in details. + (evidenceDetails, impactAnalysis, recentComments) + \n + @returns (dict) - problem details + """ + details = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{problem_id}", + params=kwargs + ).json() + + return details + + +def close_problem(cluster, tenant, problem_id, comment=""): + """Manually closes an open problem, leaving a comment. + \n + @param cluster (dict) - Dynatrace Cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace Tenant (as taken from variable set)\n + @param problem_id (str) - ID of the problem to close\n + @param comment (str) - closing comment + \n + @returns Response - HTTP response for the request + """ + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{problem_id}/close", + method=rh.HTTP.POST, + json=dict(message=comment) + ) + + return response + + +def get_all_comments(cluster, tenant, problem_id, **kwargs): + """Gets a list of all comments of a problem. + \n + @param cluster (dict) - Dynatrace Cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace Tenant (as taken from variable set)\n + @param problem_id (str) - ID of the problem to close + \n + @kwargs pageSize (int) - affects number of API calls + \n + @returns list - list of comments + """ + comments = rh.get_results_whole( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{problem_id}/comments", + api_version=2, + item="comments", + **kwargs + ).get("comments") + + return comments + + +def get_comment(cluster, tenant, problem_id, comment_id): + """Gets a comment from a Problem. + \n + @param cluster (dict) - Dynatrace Cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace Tenant (as taken from variable set)\n + @param problem_id (str) - ID of the problem containing the comment\n + @param comment_id (str) - ID fo the comment to retrieve + \n + @returns dict - comment details + """ + comment = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{problem_id}/comments/{comment_id}" + ).json() + + return comment + + +def add_comment(cluster, tenant, problem_id, **kwargs): + """Adds a comment to a problem. + \n + @param cluster (dict) - Dynatrace Cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace Tenant (as taken from variable set)\n + @param problem_id (str) - ID of the problem to add the comment to + \n + @kwargs comment (str) - comment content\n + @kwargs context (str) - comment context. added under "via ..." + \n + @returns Response - HTTP response for the request + """ + comment = kwargs.get("comment") if "comment" in kwargs else "" + context = kwargs.get("context") if "context" in kwargs else "" + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{problem_id}/comments", + method=rh.HTTP.POST, + json=dict(comment=comment, context=context) + ) + + return response + + +def update_comment(cluster, tenant, problem_id, comment_id, **kwargs): + """Updates an existing comment of a problem. + \n + @param cluster (dict) - Dynatrace Cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace Tenant (as taken from variable set)\n + @param problem_id (str) - ID of the problem containing the comment\n + @param comment_id (str) - ID of the comment to update + \n + @kwargs comment (str) - comment content\n + @kwargs context (str) - comment context. added under "via ..." + \n + @returns Response - HTTP response for the request + """ + comment = get_comment(cluster, tenant, problem_id, comment_id) + + if "comment" in kwargs: + comment["comment"] = kwargs.get("comment") + if "context" in kwargs: + comment["context"] = kwargs.get("context") + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{problem_id}/comments/{comment_id}", + method=rh.HTTP.PUT, + json=comment + ) + + return response + + +def delete_comment(cluster, tenant, problem_id, comment_id): + """Deletes a comment from a problem. + \n + @param cluster (dict) - Dynatrace Cluster (as taken from variable set)\n + @param tenant (str) - name of Dynatrace Tenant (as taken from variable set)\n + @param problem_id (str) - ID of the problem containing the comment\n + @param comment_id (str) - ID of the comment to delete + \n + @returns Response - HTTP response for the request + """ + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{problem_id}/comments/{comment_id}", + method=rh.HTTP.DELETE + ) + + return response From 0069fd80d0d5d2c68852f80f3c53f5d5deb8f473 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 27 Nov 2020 07:57:11 +0000 Subject: [PATCH 144/204] PAF-59: test suite for Problems API --- tests/test_problems.py | 187 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 tests/test_problems.py diff --git a/tests/test_problems.py b/tests/test_problems.py new file mode 100644 index 0000000..9b6e041 --- /dev/null +++ b/tests/test_problems.py @@ -0,0 +1,187 @@ +"""Test Suite for the Extensions API""" +import unittest +from user_variables import FULL_SET # pylint: disable=import-error +from tests import tooling_for_test as testtools +from dynatrace.framework.request_handler import TenantAPIs, HTTP +from dynatrace.tenant import problems + +CLUSTER = FULL_SET["mockserver1"] +TENANT = "tenant1" +URL_PATH = str(TenantAPIs.PROBLEMS) +PROBLEM_ID = "-123456789_987654321V2" +COMMENT_ID = "123456789" +REQUEST_DIR = "tests/mockserver_payloads/requests/problems" +RESPONSE_DIR = "tests/mockserver_payloads/responses/problems" + + +class TestGetProblems(unittest.TestCase): + """Test cases for fetching problems, comments, and their details""" + + def test_get_all_problems(self): + """Tests fetching all problems in a tenant""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + response_file=response_file, + request_type=str(HTTP.GET) + ) + + result = problems.get_all_problems(CLUSTER, TENANT) + expected_result = testtools.expected_payload(response_file).get('problems') + + self.assertEqual(result, expected_result) + + def test_get_problem_details(self): + """Tests fetching the details of a single problem""" + response_file = f"{RESPONSE_DIR}/get_one.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{PROBLEM_ID}", + response_file=response_file, + request_type=str(HTTP.GET) + ) + + result = problems.get_problem_details(CLUSTER, TENANT, PROBLEM_ID) + expected_result = testtools.expected_payload(response_file) + + self.assertEqual(result, expected_result) + + def test_get_problem_count(self): + """Tests fetching the total number of problems""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + response_file=response_file, + request_type=str(HTTP.GET) + ) + + result = problems.get_problem_count(CLUSTER, TENANT) + + self.assertEqual(result, 2) + + def test_get_all_comments(self): + """Tests fetching all comments of a problem""" + response_file = f"{RESPONSE_DIR}/get_comments.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{PROBLEM_ID}/comments", + response_file=response_file, + request_type=str(HTTP.GET) + ) + + result = problems.get_all_comments(CLUSTER, TENANT, PROBLEM_ID) + expected_result = testtools.expected_payload(response_file).get('comments') + + self.assertEqual(result, expected_result) + + def test_get_comment_details(self): + """Tests fetching a single comment of a problem""" + response_file = f"{RESPONSE_DIR}/get_comment.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{PROBLEM_ID}/comments/{COMMENT_ID}", + response_file=response_file, + request_type=str(HTTP.GET) + ) + + result = problems.get_comment(CLUSTER, TENANT, PROBLEM_ID, COMMENT_ID) + expected_result = testtools.expected_payload(response_file) + + self.assertEqual(result, expected_result) + + +class TestModifyProblems(unittest.TestCase): + """Test cases for making changes to problems and comments""" + + def test_close_problem(self): + """Tests the manual closing of a problem""" + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{PROBLEM_ID}/close", + response_code=204, + request_type=str(HTTP.POST) + ) + + result = problems.close_problem(CLUSTER, TENANT, PROBLEM_ID) + + self.assertEqual(result.status_code, 204) + + def test_add_comment(self): + """Tests adding a comment to a problem""" + request_file = f"{REQUEST_DIR}/comment.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{PROBLEM_ID}/comments", + request_file=request_file, + response_code=200, + request_type=str(HTTP.POST) + ) + + result = problems.add_comment( + cluster=CLUSTER, + tenant=TENANT, + problem_id=PROBLEM_ID, + comment="Test comment", + context="Test" + ) + + self.assertEqual(result.status_code, 200) + + def test_update_comment(self): + """Tests updating an existing comment of problem""" + response_file = f"{RESPONSE_DIR}/get_comment.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{PROBLEM_ID}/comments/{COMMENT_ID}", + request_type=str(HTTP.GET), + response_file=response_file, + mock_id="call1" + ) + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{PROBLEM_ID}/comments/{COMMENT_ID}", + request_file=response_file, + response_code=204, + request_type=str(HTTP.PUT), + mock_id="call2" + ) + + result = problems.update_comment(CLUSTER, TENANT, PROBLEM_ID, COMMENT_ID) + + self.assertEqual(result.status_code, 204) + + def test_delete_comment(self): + """Tests deleting a comment from a problem""" + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{PROBLEM_ID}/comments/{COMMENT_ID}", + request_type=str(HTTP.DELETE), + response_code=204 + ) + + result = problems.delete_comment(CLUSTER, TENANT, PROBLEM_ID, COMMENT_ID) + + self.assertEqual(result.status_code, 204) + + +if __name__ == "__main__": + unittest.main() From f53212305b1dc4e01172768eb20e10ee8d096990 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 27 Nov 2020 07:58:32 +0000 Subject: [PATCH 145/204] PAF-59: payloads for Problems API test suite --- .../requests/problems/comment.json | 4 ++ .../responses/problems/get_all.json | 70 +++++++++++++++++++ .../responses/problems/get_comment.json | 7 ++ .../responses/problems/get_comments.json | 20 ++++++ .../responses/problems/get_one.json | 32 +++++++++ 5 files changed, 133 insertions(+) create mode 100644 tests/mockserver_payloads/requests/problems/comment.json create mode 100644 tests/mockserver_payloads/responses/problems/get_all.json create mode 100644 tests/mockserver_payloads/responses/problems/get_comment.json create mode 100644 tests/mockserver_payloads/responses/problems/get_comments.json create mode 100644 tests/mockserver_payloads/responses/problems/get_one.json diff --git a/tests/mockserver_payloads/requests/problems/comment.json b/tests/mockserver_payloads/requests/problems/comment.json new file mode 100644 index 0000000..c36b9ff --- /dev/null +++ b/tests/mockserver_payloads/requests/problems/comment.json @@ -0,0 +1,4 @@ +{ + "comment": "Test comment", + "context": "Test" +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/problems/get_all.json b/tests/mockserver_payloads/responses/problems/get_all.json new file mode 100644 index 0000000..6091a8e --- /dev/null +++ b/tests/mockserver_payloads/responses/problems/get_all.json @@ -0,0 +1,70 @@ +{ + "nextPageKey": null, + "totalCount": 2, + "problems": [ + { + "problemId": "-123456789_987654321V2", + "displayId": "P-123456", + "title": "Problem1", + "impactLevel": "INFRASTRUCTURE", + "severityLevel": "AVAILABILITY", + "status": "OPEN", + "affectedEntities": [ + { + "entityId": { + "id": "HOST-123456789", + "type": "HOST" + }, + "name": "DemoHost" + } + ], + "impactedEntities": [ + { + "entityId": { + "id": "HOST-123456789", + "type": "HOST" + }, + "name": "DemoHost" + } + ], + "rootCauseEntity": null, + "managementZones": [], + "entityTags": [], + "problemFilters": [], + "startTime": 1605168880219, + "endTime": -1 + }, + { + "problemId": "987654321_123456789V2", + "displayId": "P-98765", + "title": "Problem2", + "impactLevel": "APPLICATION", + "severityLevel": "AVAILABILITY", + "status": "OPEN", + "affectedEntities": [ + { + "entityId": { + "id": "HTTP_CHECK-123456789", + "type": "HTTP_CHECK" + }, + "name": "Demo Test" + } + ], + "impactedEntities": [ + { + "entityId": { + "id": "HTTP_CHECK-123456789", + "type": "HTTP_CHECK" + }, + "name": "Demo Test" + } + ], + "rootCauseEntity": null, + "managementZones": [], + "entityTags": [], + "problemFilters": [], + "startTime": 1605169224250, + "endTime": -1 + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/problems/get_comment.json b/tests/mockserver_payloads/responses/problems/get_comment.json new file mode 100644 index 0000000..5f74e8e --- /dev/null +++ b/tests/mockserver_payloads/responses/problems/get_comment.json @@ -0,0 +1,7 @@ +{ + "id": "123456789", + "createdAtTimestamp": 1606412761265, + "content": "test comment 1", + "authorName": "radu.stefan@mocktest.com", + "context": "Python API" +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/problems/get_comments.json b/tests/mockserver_payloads/responses/problems/get_comments.json new file mode 100644 index 0000000..54b8f19 --- /dev/null +++ b/tests/mockserver_payloads/responses/problems/get_comments.json @@ -0,0 +1,20 @@ +{ + "totalCount": 2, + "pageSize": 10, + "comments": [ + { + "id": "123456789", + "createdAtTimestamp": 1606412761265, + "content": "test comment 1", + "authorName": "radu.stefan@mocktest.com", + "context": "Python API" + }, + { + "id": "987654321", + "createdAtTimestamp": 1606411416557, + "content": "", + "authorName": "radu.stefan@mocktest.com", + "context": "dynatrace-problem-close" + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/problems/get_one.json b/tests/mockserver_payloads/responses/problems/get_one.json new file mode 100644 index 0000000..3ba9349 --- /dev/null +++ b/tests/mockserver_payloads/responses/problems/get_one.json @@ -0,0 +1,32 @@ +{ + "problemId": "-123456789_987654321V2", + "displayId": "P-123456", + "title": "Problem1", + "impactLevel": "INFRASTRUCTURE", + "severityLevel": "AVAILABILITY", + "status": "OPEN", + "affectedEntities": [ + { + "entityId": { + "id": "HOST-123456789", + "type": "HOST" + }, + "name": "DemoHost" + } + ], + "impactedEntities": [ + { + "entityId": { + "id": "HOST-123456789", + "type": "HOST" + }, + "name": "DemoHost" + } + ], + "rootCauseEntity": null, + "managementZones": [], + "entityTags": [], + "problemFilters": [], + "startTime": 1605168880219, + "endTime": -1 +} \ No newline at end of file From 0afc83f9060ce17e51a0df33346574125aff644b Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 6 Dec 2020 18:06:42 +0000 Subject: [PATCH 146/204] PAF-42: Added logging to request handler --- dynatrace/framework/request_handler.py | 67 +++++++++++++++++++++----- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 6de4379..e2cbffd 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -3,10 +3,13 @@ import time import functools import requests +from copy import deepcopy +from dynatrace.framework import logging from dynatrace.framework.exceptions import InvalidAPIResponseException, ManagedClusterOnlyException requests.packages.urllib3.disable_warnings() # pylint: disable=no-member +logger = logging.get_logger(__name__) HTTPS_STR = "https://" @@ -108,6 +111,9 @@ def wrapper(*args, **kwargs): req_limit = int(response.headers.get('x-ratelimit-limit')) # If 75% requests already made, slow down if req_remaining/req_limit <= 0.25: + logger.info( + "API rate limit approaching. Introducing 1 sec delay between calls." + ) time.sleep(1) return response @@ -135,6 +141,7 @@ def make_api_call(cluster, endpoint, tenant=None, method=HTTP.GET, **kwargs): # Set the right URL for the operation url = f"{generate_tenant_url(cluster, tenant)}{endpoint}" \ if tenant else f"{HTTPS_STR}{cluster['url']}{endpoint}" + logger.debug(f"URL used for API call: {url}") # Get correct token for the operation if 'onpremise' in str(endpoint) or 'cluster' in str(endpoint): @@ -143,6 +150,12 @@ def make_api_call(cluster, endpoint, tenant=None, method=HTTP.GET, **kwargs): else: headers = dict(Authorization=f"Api-Token {cluster['api_token'][tenant]}") + logger.debug("API call details:") + call_details = deepcopy(locals()) + call_details["cluster"]["api_token"][tenant] = "*" * 12 + call_details["cluster"]["cluster_token"] = "*" * 12 + call_details["headers"]["Authorization"] = "*" * 12 + logger.debug(call_details) # Loop to retry in case of rate limits while True: response = requests.request( @@ -177,15 +190,22 @@ def get_results_whole(cluster, tenant, endpoint, api_version, **kwargs): if api_version == 2: is_v2 = True if 'item' not in kwargs: - raise ValueError("For V2 APIs you must provide collected item.") + try: + raise ValueError("For V2 APIs you must provide collected item.") + except ValueError: + logger.exception("Error: item missing from V2 API call.") + raise item = kwargs['item'] results = {} + logger.debug(f"Using V2 pagination for API to collect {item}") else: is_v2 = False results = [] + logger.debug("Using V1 pagination for API") while cursor: if cursor != 1: + logger.debug(f"Getting next page of results. Cursor is {cursor}") if not is_v2 or endpoint == TenantAPIs.ONEAGENTS: # V1 and OneAgents require all other query params are preserved kwargs['nextPageKey'] = cursor @@ -234,19 +254,26 @@ def get_results_by_page(cluster, tenant, endpoint, api_version, **kwargs): if api_version == 2: is_v2 = True if 'item' not in kwargs: - raise ValueError("For is_v2 APIs you must provide collected item.") + try: + raise ValueError("For is_v2 APIs you must provide collected item.") + except ValueError: + logger.exception("Error: item missing from V2 API call.") + raise item = kwargs['item'] + logger.debug(f"Using V2 pagination for API to collect {item}") else: + logger.debug("Using V1 pagination for API") is_v2 = False while cursor: if cursor != 1: - # V2 requires all other query params are removed - if is_v2: - kwargs = dict(nextPageKey=cursor) + logger.debug(f"Getting next page of results. Cursor is {cursor}") # V1 requires all other query params are preserved - else: + if not is_v2 or endpoint == TenantAPIs.ONEAGENTS: kwargs['nextPageKey'] = cursor + # V2 requires all other query params are removed + else: + kwargs = dict(nextPageKey=cursor) response = make_api_call( cluster=cluster, @@ -271,38 +298,54 @@ def check_response(response): @param response - The response variable returned from a request\n ''' + logger.debug("Validating the response for the API call.") + logger.debug(f"Response: {response}") headers = response.headers if response.status_code == 429: - print("Endpoint request limit of " - f"{headers['x-ratelimit-limit']} was reached!") + logger.warn("Endpoint request limit of " + f"{headers['x-ratelimit-limit']} was reached!") # Wait until the limit resets and try again time_to_wait = int(headers['x-ratelimit-reset'])/1000000 - time.time() # Check that there's actually time to wait if time_to_wait > 0: - print(f"Waiting {time_to_wait} sec until the limit resets.") + logger.warn(f"Waiting {time_to_wait} sec until the limit resets.") time.sleep(float(time_to_wait)) return False if not 200 <= response.status_code <= 299: - raise InvalidAPIResponseException( - f"Response Error:\n{response.url}\n{response.status_code}\n{response.text}") + try: + raise InvalidAPIResponseException( + "Response Error:\n" + f"{response.url}\n{response.status_code}\n{response.text}" + ) + except InvalidAPIResponseException: + logger.exception("Error: Invalid API response.") + raise return True def check_managed(cluster): """Checks if the Cluster Operation is valid (Managed) for the current cluster""" + logger.debug("Checking that the cluster is Managed.") if not cluster['is_managed']: - raise ManagedClusterOnlyException() + try: + raise ManagedClusterOnlyException() + except ManagedClusterOnlyException: + logger.exception("Error: Managed operation attempted on SaaS cluster.") + raise def generate_tenant_url(cluster, tenant): """Generate URL based on SaaS or Managed""" url = HTTPS_STR if cluster["is_managed"]: + logger.debug("Generating URL for a Managed cluster.") url += cluster['url'] + "/e/" + cluster['tenant'][tenant] else: + logger.debug("Generating URL for a SaaS cluster.") url += cluster['tenant'][tenant] + "." + cluster['url'] + return url From fdbd2b16027b30a7b8c103825cc2ee596d6a8404 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 6 Dec 2020 18:07:06 +0000 Subject: [PATCH 147/204] PAF-42: Added settings related to logging --- dynatrace/framework/settings.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dynatrace/framework/settings.py b/dynatrace/framework/settings.py index deb3be0..c2dd5c8 100644 --- a/dynatrace/framework/settings.py +++ b/dynatrace/framework/settings.py @@ -7,8 +7,12 @@ DefaultSettings = { - 'LOG_LEVEL': None, - 'LOG_DIR': "logs/", + 'LOG_LEVEL': "ERROR", + 'LOG_DIR': None, + 'LOG_OUTPUT': [ + "CONSOLE", + ], + 'LOG_ENABLED': True, # ROLE TYPE KEYS # access_env From cf5da1bb182514b95e8f36949e0f540d408b1ba9 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 6 Dec 2020 18:07:47 +0000 Subject: [PATCH 148/204] PAF-42: logging module. setup for framework logging --- dynatrace/framework/logging.py | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 dynatrace/framework/logging.py diff --git a/dynatrace/framework/logging.py b/dynatrace/framework/logging.py new file mode 100644 index 0000000..fa9954a --- /dev/null +++ b/dynatrace/framework/logging.py @@ -0,0 +1,49 @@ +"""Module for logging throughout the framework.""" +import logging +from logging import handlers +from dynatrace.framework import settings + +logging.root.setLevel(logging.NOTSET) + + +def get_logger(name=__name__): + """Sets up a logger and returns it for use throughout the framework. + Actual configuration parameters are exposed in framework settings. + \n + @param name (str) - name of the logger. defaults to __name__ + \n + @returns Logger - logger to be used in framework + """ + enabled = settings.get_setting("LOG_ENABLED") + output = settings.get_setting("LOG_OUTPUT") + folder = settings.get_setting("LOG_DIR") + level = settings.get_setting("LOG_LEVEL") + logger = logging.getLogger(name) + + log_format = logging.Formatter( + fmt="[%(asctime)s][%(module)s][%(funcName)s][%(levelname)s] %(message)s", + datefmt="%Y-%b-%d %H:%M:%S" + ) + + if enabled: + if "FILE" in output: + file_handler = handlers.RotatingFileHandler( + filename=f"{folder}/Framework.log", + delay=True, + maxBytes=1000000, + backupCount=5 + ) + file_handler.setFormatter(log_format) + file_handler.setLevel(level) + logger.addHandler(file_handler) + + if "CONSOLE" in output: + console_handler = logging.StreamHandler() + console_handler.setFormatter(log_format) + console_handler.setLevel(level) + logger.addHandler(console_handler) + else: + # Essentially disables logging + logger.setLevel(logging.CRITICAL+1) + + return logger From 0ce7d12933a58d0015deceb64dd8bd8bb5b0fe6e Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 6 Dec 2020 18:08:14 +0000 Subject: [PATCH 149/204] PAF-42: added logging for entities module --- dynatrace/tenant/entities.py | 65 +++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index 949e089..964d013 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -1,8 +1,11 @@ """Module for Entities API operations""" from enum import Enum, auto +from dynatrace.framework import logging from dynatrace.framework import request_handler as rh +logger = logging.get_logger(__name__) + class EntityTypes(Enum): """Accepted values for EntityType arguments""" @@ -128,6 +131,7 @@ def get_entities_tenantwide(cluster, tenant, entity_type, **kwargs): else: kwargs['entitySelector'] = f'type({entity_type})' + logger.info(f"Getting whole result set for entities in {tenant} tenant") response = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -155,6 +159,7 @@ def get_entities_clusterwide(cluster, entity_type, aggregated=True, **kwargs): split_entities = {} all_entities = [] + logger.info("Getting whole result set for entities in cluster") for tenant in cluster['tenant']: tenant_entities = get_entities_tenantwide( cluster=cluster, @@ -184,6 +189,7 @@ def get_entities_setwide(full_set, entity_type, aggregated=True, **kwargs): split_entities = {} all_entities = [] + logger.info("Getting whole result set for entities in all clusters") for cluster in full_set: cluster_entities = get_entities_clusterwide( cluster=full_set[cluster], @@ -216,6 +222,7 @@ def get_entities_by_page(cluster, tenant, entity_type, **kwargs): else: kwargs['entitySelector'] = f'type({entity_type})' + logger.info(f"Getting paged result set for entities in {tenant} tenant") response = rh.get_results_by_page( cluster=cluster, tenant=tenant, @@ -248,6 +255,7 @@ def get_entity(cluster, tenant, entity_id, **kwargs): else: kwargs['entitySelector'] = f'entityId({entity_id})' + logger.info(f"Getting entity details for ID(s) {entity_id}") response = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -287,6 +295,7 @@ def get_entity_count_tenantwide(cluster, tenant, entity_type, **kwargs): else: kwargs['entitySelector'] = f'type({entity_type})' + logger.info(f"Getting entity count from {tenant} tenant") response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -309,6 +318,7 @@ def get_entity_count_clusterwide(cluster, entity_type, **kwargs): @return - number of entities """ count = 0 + logger.info("Getting entity count from cluster") for tenant in cluster['tenant']: count += get_entity_count_tenantwide( cluster=cluster, @@ -331,6 +341,7 @@ def get_entity_count_setwide(full_set, entity_type, **kwargs): @return - number of entities """ count = 0 + logger.info("Getting entity count from all clusters") for cluster in full_set: count += get_entity_count_clusterwide( cluster=full_set[cluster], @@ -355,13 +366,26 @@ def add_tags(cluster, tenant, tag_list, **kwargs): """ # Sanity checking, error handling if not tag_list: - raise TypeError("No tags provided") + try: + raise TypeError("No tags provided") + except TypeError: + logger.exception("Error: No tags provided") + raise if not isinstance(tag_list, list): - raise TypeError("tags_list is not a list") + try: + raise TypeError("tags_list is not a list") + except TypeError: + logger.exception("Error: tags_list must be a list") + raise if 'type' not in kwargs['entitySelector'] \ and 'entityId' not in kwargs['entitySelector']: - raise ValueError("entitySelector must have at least type or entityId") + try: + raise ValueError("entitySelector must have at least type or entityId") + except ValueError: + logger.exception("Error: entitySelector missing required values") + raise + logger.info("Adding tags to entities") response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -390,17 +414,29 @@ def delete_tag(cluster, tenant, tag_key, tag_value=None, **kwargs): """ # Sanity checking, error handling if not tag_key: - raise TypeError("No tag key provided") + try: + raise TypeError("No tag key provided") + except TypeError: + logger.exception("Error: Must provide a tag key") + raise if 'type' not in kwargs['entitySelector'] \ and 'entityId' not in kwargs['entitySelector']: - raise ValueError("entitySelector must have at least type or entityId") + try: + raise ValueError("entitySelector must have at least type or entityId") + except ValueError: + logger.exception("Error: entitySelector missing required values") + raise # Set params for tag key & value kwargs['key'] = tag_key if tag_value == "all": kwargs['deleteAllWithKey'] = True + logger.info(f"Deleting all {tag_key} tags from entities") elif tag_value: kwargs['value'] = tag_value + logger.info(f"Deleting {tag_key}:{tag_value} tags from entities") + else: + logger.info(f"Deleting {tag_key} tag from entities.") response = rh.make_api_call( cluster=cluster, @@ -431,13 +467,18 @@ def custom_device(cluster, tenant, json_data): @throws ValueError - if mandatory properties missing from JSON data """ # Sanity checking, error handling - if not json_data.get('customDeviceId') or not json_data.get('displayName'): - raise ValueError("JSON data is missing Device ID and/or Name.") - # json_data.type can be NoneType when device already exists - if not get_entity(cluster, tenant, json_data.get('customDeviceId')) \ - and not json_data.get('type'): - raise ValueError("type must be in JSON data when creating a device") - + try: + if not json_data.get('customDeviceId') or not json_data.get('displayName'): + raise ValueError("JSON data is missing Device ID and/or Name.") + # json_data.type can be NoneType when device already exists + if not get_entity(cluster, tenant, json_data.get('customDeviceId')) \ + and not json_data.get('type'): + raise ValueError("type must be in JSON data when creating a device") + except ValueError: + logger.exception("Error: Missing mandatory details.") + raise + + logger.info("Creating/updating custom device.") response = rh.make_api_call( cluster=cluster, tenant=tenant, From f01ec6573ef137534e09da561b68b3fc4a005875 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 6 Dec 2020 20:47:23 +0000 Subject: [PATCH 150/204] PAF-42: name is mandatory for get_logger now --- dynatrace/tenant/extensions.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index e293074..78afd5e 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -1,8 +1,9 @@ """Operations Interacting with Dynatrace Extensions API""" -from dynatrace.framework import request_handler as rh +from dynatrace.framework import request_handler as rh, logging from dynatrace.tenant import metrics ENDPOINT = rh.TenantAPIs.EXTENSIONS +logger = logging.get_logger(__name__) def get_all_extensions(cluster, tenant, page_size=200): @@ -16,6 +17,7 @@ def get_all_extensions(cluster, tenant, page_size=200): \n @returns list - list of extensions """ + logger.info(f"Getting all extensions in {tenant} tenant") extension_list = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -37,6 +39,7 @@ def get_extension_details(cluster, tenant, extension_id): \n @returns (dict) - JSON response containing extension details """ + logger.info(f"Getting extension details for {extension_id}") details = rh.make_api_call( cluster=cluster, endpoint=f"{ENDPOINT}/{extension_id}", @@ -55,6 +58,7 @@ def get_extension_metrics(cluster, tenant, extension_id): \n @returns list - list of metric IDs """ + logger.info(f"Getting metrics collected by extension {extension_id}") metric_group = get_extension_details(cluster, tenant, extension_id).get('metricGroup') ext_metrics = metrics.get_metric_descriptor( cluster=cluster, @@ -75,6 +79,7 @@ def get_extension_global_config(cluster, tenant, extension_id): \n @returns dict - global configuration """ + logger.info(f"Getting global configuration for extension {extension_id}") config = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -95,6 +100,9 @@ def get_extension_instance_config(cluster, tenant, extension_id, instance_id): \n @returns dict - instance configuration """ + logger.info( + f"Getting configuration for instance {instance_id} on extension {extension_id}" + ) config = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -115,6 +123,7 @@ def get_extension_states(cluster, tenant, extension_id): \n @returns list - states/instances of this extension """ + logger.info(f"Getting states for extension {extension_id}") states = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -136,6 +145,7 @@ def get_extension_instances(cluster, tenant, extension_id): \n @returns list - configuration instances for this extension """ + logger.info(f"Getting instances for extension {extension_id}") instances = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -160,7 +170,7 @@ def enable_global_config(cluster, tenant, extension_id): config = get_extension_global_config(cluster, tenant, extension_id) config['enabled'] = True - + logger.info(f"Enabling global config for extension {extension_id}") response = update_global_config(cluster, tenant, extension_id, config) return response @@ -179,7 +189,7 @@ def disable_global_config(cluster, tenant, extension_id): config = get_extension_global_config(cluster, tenant, extension_id) config['enabled'] = False - + logger.info(f"Disabling global config for extension {extension_id}") response = update_global_config(cluster, tenant, extension_id, config) return response @@ -196,6 +206,7 @@ def update_global_config(cluster, tenant, extension_id, config): \n @returns dict - HTTP response to request """ + logger.info(f"Updating global config for extension {extension_id}") response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -232,6 +243,7 @@ def enable_instance_config(cluster, tenant, extension_id, instance_id): if 'activeGate' in config: config['extensionId'] = extension_id + logger.info(f"Enabling config for instance {instance_id} of {extension_id}") response = update_instance_config( cluster, tenant, extension_id, instance_id, config ) @@ -264,6 +276,7 @@ def disable_instance_config(cluster, tenant, extension_id, instance_id): if 'activeGate' in config: config['extensionId'] = extension_id + logger.info(f"Disabling config for instance {instance_id} of {extension_id}") response = update_instance_config( cluster, tenant, extension_id, instance_id, config ) @@ -283,6 +296,7 @@ def update_instance_config(cluster, tenant, extension_id, instance_id, config): \n @returns dict - HTTP response to request """ + logger.info(f"Updating config for instance {instance_id} of {extension_id}") response = rh.make_api_call( cluster=cluster, tenant=tenant, From 455fc910f86120c78597f372cf2f8e05a3604199 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sun, 6 Dec 2020 20:47:49 +0000 Subject: [PATCH 151/204] PAF-42: name is mandatory for get_logger now --- dynatrace/framework/logging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dynatrace/framework/logging.py b/dynatrace/framework/logging.py index fa9954a..0b370c1 100644 --- a/dynatrace/framework/logging.py +++ b/dynatrace/framework/logging.py @@ -6,7 +6,7 @@ logging.root.setLevel(logging.NOTSET) -def get_logger(name=__name__): +def get_logger(name): """Sets up a logger and returns it for use throughout the framework. Actual configuration parameters are exposed in framework settings. \n @@ -18,6 +18,7 @@ def get_logger(name=__name__): output = settings.get_setting("LOG_OUTPUT") folder = settings.get_setting("LOG_DIR") level = settings.get_setting("LOG_LEVEL") + logger = logging.getLogger(name) log_format = logging.Formatter( From f75782d81bb2787deb93031350c8839f309c617e Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Mon, 7 Dec 2020 08:25:38 +0000 Subject: [PATCH 152/204] PAF-42: changed back default logs directory. tests depend on it --- dynatrace/framework/settings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dynatrace/framework/settings.py b/dynatrace/framework/settings.py index c2dd5c8..fd3b1b0 100644 --- a/dynatrace/framework/settings.py +++ b/dynatrace/framework/settings.py @@ -7,12 +7,12 @@ DefaultSettings = { - 'LOG_LEVEL': "ERROR", - 'LOG_DIR': None, - 'LOG_OUTPUT': [ + "LOG_LEVEL": "ERROR", + "LOG_DIR": "logs/", + "LOG_OUTPUT": [ "CONSOLE", ], - 'LOG_ENABLED': True, + "LOG_ENABLED": True, # ROLE TYPE KEYS # access_env From 0aed6e61e983f779d2119201cbd0031c5b87d881 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Mon, 7 Dec 2020 08:25:56 +0000 Subject: [PATCH 153/204] PAF-42: added logging for metrics.py --- dynatrace/tenant/metrics.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index f7bc92a..925d853 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -1,8 +1,9 @@ """Module for interacting with the Metrics API""" -from dynatrace.framework import request_handler as rh +from dynatrace.framework import request_handler as rh, logging from dynatrace.framework.exceptions import InvalidAPIResponseException ENDPOINT = str(rh.TenantAPIs.METRICS) +logger = logging.get_logger(__name__) def get_metric_descriptor(cluster, tenant, **kwargs): @@ -15,6 +16,7 @@ def get_metric_descriptor(cluster, tenant, **kwargs): \n @returns list - list of metric descriptors mathing the metricSelector """ + logger.info("Getting metric descriptors") descriptors = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -35,6 +37,7 @@ def get_metric_count(cluster, tenant, **kwargs): \n @returns int - Number of metrics matching the metricSelector """ + logger.info("Getting the total metric count for the query.") count = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -64,6 +67,7 @@ def get_metric_data(cluster, tenant, **kwargs): next_page_key = 1 results = {} + logger.info("Getting metric datapoints") while next_page_key: # Upon subsequent calls, clear all other params if next_page_key != 1: @@ -76,7 +80,9 @@ def get_metric_data(cluster, tenant, **kwargs): params=kwargs) except InvalidAPIResponseException as err: if 'metric key that could not be resolved in the metric registry' in str(err): + logger.warn("Invalid metric ID encountered. Returning results so far.") break + logger.exception("Error: Invalid API response") raise for result in response.json().get('result'): @@ -103,6 +109,7 @@ def get_metric_dimension_count(cluster, tenant, metric_selector): \n @returns int - the sum total of dimensions across all matched metrics """ + logger.info("Getting dimension count for metric(s)") details = get_metric_descriptor( cluster=cluster, tenant=tenant, @@ -131,6 +138,7 @@ def get_metric_estimated_ddus(cluster, tenant, metric_selector): \n @returns (float) - total number of yearly DDUs """ + logger.info("Getting DDUs for metric(s)") return get_metric_dimension_count( cluster=cluster, tenant=tenant, @@ -149,6 +157,7 @@ def ingest_metrics(cluster, tenant, payload): \n @returns (dict) - response to HTTP request """ + logger.info("Sending metrics to Dynatrace") return rh.make_api_call( cluster=cluster, tenant=tenant, From 76c3deaec24d450a3922d252044da38ba5f6e535 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 9 Dec 2020 18:48:47 +0000 Subject: [PATCH 154/204] PAF-42: added logging for oneagents.py --- dynatrace/tenant/oneagents.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index bac4643..5ebdb98 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -1,6 +1,8 @@ """Module for OneAgent operations.""" -import dynatrace.framework.request_handler as rh +from dynatrace.framework import request_handler as rh, logging + +logger = logging.get_logger(__name__) def get_host_units_tenantwide(cluster, tenant, **kwargs): @@ -13,6 +15,7 @@ def get_host_units_tenantwide(cluster, tenant, **kwargs): """ host_units = 0 + logger.info(f"Getting hosts from tenant {tenant}") host_list = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -21,6 +24,7 @@ def get_host_units_tenantwide(cluster, tenant, **kwargs): **kwargs ) + logger.info("Adding up host units") for host in host_list: host_units += round(host['consumedHostUnits'], ndigits=3) @@ -39,6 +43,7 @@ def get_host_units_clusterwide(cluster, aggregated=True, **kwargs): total_host_units = 0 host_units = {} + logger.info("Getting host units for the whole cluster") for tenant in cluster['tenant']: tenant_host_units = get_host_units_tenantwide( cluster=cluster, @@ -63,6 +68,7 @@ def get_host_units_setwide(full_set, aggregated=True, **kwargs): total_host_units = 0 host_units = {} + logger.info("Getting host units for the whole set") for cluster in full_set: cluster_host_units = get_host_units_clusterwide( cluster=full_set[cluster], @@ -83,6 +89,7 @@ def get_oneagents_tenantwide(cluster, tenant, **kwargs): @returns - list of OneAgents """ + logger.info(f"Getting OneAgents from tenant {tenant}") return rh.get_results_whole( cluster=cluster, tenant=tenant, From 6461f2eb0706f1ebf909a0340dc2bf9b1567a17e Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 9 Dec 2020 18:54:56 +0000 Subject: [PATCH 155/204] PAF-42: added logging for problems.py --- dynatrace/tenant/problems.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/dynatrace/tenant/problems.py b/dynatrace/tenant/problems.py index dfe92d6..d204fc6 100644 --- a/dynatrace/tenant/problems.py +++ b/dynatrace/tenant/problems.py @@ -1,7 +1,8 @@ """Module for interactions with the Problems (V2) API""" -from dynatrace.framework import request_handler as rh +from dynatrace.framework import request_handler as rh, logging ENDPOINT = str(rh.TenantAPIs.PROBLEMS) +logger = logging.get_logger(__name__) def get_all_problems(cluster, tenant, **kwargs): @@ -12,6 +13,7 @@ def get_all_problems(cluster, tenant, **kwargs): \n @returns list - list of problems """ + logger.info(f"Getting problems from tenant {tenant}") problems_list = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -32,6 +34,7 @@ def get_problem_count(cluster, tenant, **kwargs): \n @returns int - number of problems """ + logger.info(f"Getting the total problem count in tenant {tenant}") problems_list = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -55,6 +58,7 @@ def get_problem_details(cluster, tenant, problem_id, **kwargs): \n @returns (dict) - problem details """ + logger.info(f"Getting problem details for problem {problem_id}") details = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -75,6 +79,8 @@ def close_problem(cluster, tenant, problem_id, comment=""): \n @returns Response - HTTP response for the request """ + logger.info(f"Closing problem {problem_id}") + logger.info(f"Closing comment: {comment}") response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -97,6 +103,7 @@ def get_all_comments(cluster, tenant, problem_id, **kwargs): \n @returns list - list of comments """ + logger.info(f"Getting comments from problem {problem_id}") comments = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -119,6 +126,7 @@ def get_comment(cluster, tenant, problem_id, comment_id): \n @returns dict - comment details """ + logger.info(f"Getting details for comment {comment_id} from problem {problem_id}") comment = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -140,6 +148,7 @@ def add_comment(cluster, tenant, problem_id, **kwargs): \n @returns Response - HTTP response for the request """ + logger.info(f"Adding comment to problem {problem_id}") comment = kwargs.get("comment") if "comment" in kwargs else "" context = kwargs.get("context") if "context" in kwargs else "" @@ -167,6 +176,7 @@ def update_comment(cluster, tenant, problem_id, comment_id, **kwargs): \n @returns Response - HTTP response for the request """ + logger.info(f"Updating comment {comment_id} from problem {problem_id}") comment = get_comment(cluster, tenant, problem_id, comment_id) if "comment" in kwargs: @@ -195,6 +205,7 @@ def delete_comment(cluster, tenant, problem_id, comment_id): \n @returns Response - HTTP response for the request """ + logger.info(f"Deleting comment {comment_id} from problem {problem_id}") response = rh.make_api_call( cluster=cluster, tenant=tenant, From 1fd87027c5ff4cd48d759eb5866c01cf00926352 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 9 Dec 2020 19:18:37 +0000 Subject: [PATCH 156/204] PAF-42: merged with differences from #0d07753 (PAF-42_Logging_Module) --- dynatrace/framework/logging.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dynatrace/framework/logging.py b/dynatrace/framework/logging.py index 0b370c1..0ed010f 100644 --- a/dynatrace/framework/logging.py +++ b/dynatrace/framework/logging.py @@ -28,6 +28,10 @@ def get_logger(name): if enabled: if "FILE" in output: + if folder is None: + raise ValueError( + "Could not setup logging - missing folder from settings." + ) file_handler = handlers.RotatingFileHandler( filename=f"{folder}/Framework.log", delay=True, From 9b1f43fb9f4d338c9e3e708168bf34fcafa6490d Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Wed, 9 Dec 2020 19:19:02 +0000 Subject: [PATCH 157/204] PAF-42: merged with differences from #0d07753 (PAF-42_Logging_Module) --- dynatrace/framework/logging.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/dynatrace/framework/logging.py b/dynatrace/framework/logging.py index 0ed010f..50052b0 100644 --- a/dynatrace/framework/logging.py +++ b/dynatrace/framework/logging.py @@ -1,4 +1,5 @@ """Module for logging throughout the framework.""" +import os import logging from logging import handlers from dynatrace.framework import settings @@ -6,11 +7,13 @@ logging.root.setLevel(logging.NOTSET) -def get_logger(name): +def get_logger(name, filename="Framework.log"): """Sets up a logger and returns it for use throughout the framework. Actual configuration parameters are exposed in framework settings. \n @param name (str) - name of the logger. defaults to __name__ + @param filename (str) - name of the log file (in case of logging to file) + Defaults to Framework.log \n @returns Logger - logger to be used in framework """ @@ -32,8 +35,10 @@ def get_logger(name): raise ValueError( "Could not setup logging - missing folder from settings." ) + if not os.path.exists(folder): + os.mkedirs(folder) file_handler = handlers.RotatingFileHandler( - filename=f"{folder}/Framework.log", + filename=f"{folder}/{filename}", delay=True, maxBytes=1000000, backupCount=5 From 41c6bb96f4922b546a35c5abaedb09454220ebd5 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Mon, 14 Dec 2020 10:45:22 +0000 Subject: [PATCH 158/204] PAF-75: added endpoint for timeseries api --- dynatrace/framework/request_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 6de4379..41cf0d7 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -35,6 +35,7 @@ class TenantAPIs(Enum): Enum representing Dynatrace Tenant REST API endpoints.\n Use these values when adding the 'endpoint' argument. """ + TIMESERIES = "/api/v1/timeseries" PROBLEM_DETAILS = "/api/v1/problem/details" PROBLEM_FEED = "/api/v1/problem/feed" PROBLEM_STATUS = "/api/v1/problem/status" From b0f5b1a0e7f5c8f7049d36b988b985807e786b65 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Mon, 14 Dec 2020 10:46:04 +0000 Subject: [PATCH 159/204] PAF-75: refurbished Timeseries module --- dynatrace/tenant/timeseries.py | 81 ++++++++++++---------------------- 1 file changed, 29 insertions(+), 52 deletions(-) diff --git a/dynatrace/tenant/timeseries.py b/dynatrace/tenant/timeseries.py index a9f74c4..c4b3ef2 100644 --- a/dynatrace/tenant/timeseries.py +++ b/dynatrace/tenant/timeseries.py @@ -1,57 +1,34 @@ -"""Timerseries Operations from Environment V1 API""" +"""Timerseries Operations from Environment V1 API +Note: module contains only use cases not currently fulfilled via Metrics (V2) API +""" from dynatrace.framework import request_handler as rh -ENDPOINT = "timeseries/" +ENDPOINT = rh.TenantAPIs.TIMESERIES -def get_timeseries_list(cluster, tenant, params=None): - """Get List of Timeseries Metics""" - response = rh.make_api_call(cluster, tenant, ENDPOINT, params=params) - return response.json() - - -def get_timeseries_metric(cluster, tenant, metric, params=None): - """Get Timeseries Metric""" - # Chose to do GET, but could also be done as POST. Don't think there are any advantages to post - response = rh.make_api_call(cluster, tenant, ENDPOINT + metric, params=params) - return response.json() - - -def create_custom_metric(cluster, tenant, metric, json, params=None): - """Create a custome timeseries metric - - Args: - cluster (cluster dict): Currently selected cluster - tenant (str): tenant to pull metrics from - metric (str): selected metric to store as - json (dict): json payload required to created custom metric - params (dict, optional): [description]. Defaults to None. - - Returns: - int: response status code - """ - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=f"{ENDPOINT}{metric}", - params=params, - method=rh.HTTP.PUT, - json=json) - return response.status_code - - -def delete_custom_metic(cluster, tenant, metric): - """[summary] - - Args: - cluster (cluster dict): Currently selected cluster - tenant (str): Tenant to operate in - metric (str): custom metric to be deleted - - Returns: - [type]: [description] +def get_metric_data_with_prediction(cluster, tenant, timeseries_id, **kwargs): + """Get datapoints for a metric, including prediction. + This returns a dictionary, where the timeseries ID is a key and the value is a list + of datapoints (timestamp + data). Cannot use timeframe larger than 30 min. + \n + @param cluster (dict) - Dynatrace Cluster dictionary as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set \n + @param timeseries_id (str) - ID of the Timeseries to extract + \n + @returns dict - predicted datapoints of the timeseries """ - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - method=rh.HTTP.DELETE, - endpoint=f"{ENDPOINT}{metric}") - return response.status_code + kwargs["timeseriesIdentifier"] = timeseries_id + kwargs["includeData"] = True + kwargs["predict"] = True + if not (("startTimestamp" in kwargs and "endTimestamp" in kwargs) + or "relativeTime" in kwargs): + kwargs["relativeTime"] = "30mins" + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT, + params=kwargs + ).json() + + return response.get("dataResult").get("dataPoints") From ca9f930c509689bbc856e2a44b24756f161edde6 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Mon, 14 Dec 2020 10:46:48 +0000 Subject: [PATCH 160/204] PAF-75: test cases for timeseries API --- .../responses/timeseries/get_predict.json | 63 +++++++++++++++++++ tests/test_timeseries.py | 48 ++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 tests/mockserver_payloads/responses/timeseries/get_predict.json create mode 100644 tests/test_timeseries.py diff --git a/tests/mockserver_payloads/responses/timeseries/get_predict.json b/tests/mockserver_payloads/responses/timeseries/get_predict.json new file mode 100644 index 0000000..79373ae --- /dev/null +++ b/tests/mockserver_payloads/responses/timeseries/get_predict.json @@ -0,0 +1,63 @@ +{ + "timeseriesId": "custom.test.timeseries", + "displayName": "Custom Mock Metric", + "dimensions": [ + "SERVICE" + ], + "aggregationTypes": [ + "COUNT" + ], + "unit": "Count (count)", + "filter": "BUILTIN", + "detailedSource": "Services", + "types": [], + "dataResult": { + "dataPoints": { + "SERVICE-ABC123DEF456GHI789J": [ + [ + 1607939400000, + 523.666655307729, + 410.15412668486834, + 635.8478203297005 + ], + [ + 1607939700000, + 603.0451778473598, + 472.15260627789496, + 732.1528174479977 + ], + [ + 1607940000000, + 459.95450718505396, + 335.8356721235074, + 587.664001040329 + ], + [ + 1607940300000, + 441.4722415900446, + 322.67147758176833, + 567.8571115102844 + ], + [ + 1607940600000, + 422.50195767708374, + 284.5712246898428, + 567.8243284420648 + ], + [ + 1607940900000, + 546.0887158520202, + 400.57725605634965, + 697.3510794865584 + ] + ] + }, + "timeseriesId": "custom.test.timeseries", + "unit": "Count (count)", + "resolutionInMillisUTC": 300000, + "aggregationType": "COUNT", + "entities": { + "SERVICE-ABC123DEF456GHI789J": "/MockService" + } + } +} \ No newline at end of file diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py new file mode 100644 index 0000000..efe3e21 --- /dev/null +++ b/tests/test_timeseries.py @@ -0,0 +1,48 @@ +"""Test cases for Timeseries (V1) API operations""" + +import unittest +from tests import tooling_for_test as testtools +from user_variables import FULL_SET +from dynatrace.tenant import timeseries +from dynatrace.framework.request_handler import TenantAPIs, HTTP + +CLUSTER = FULL_SET["mockserver1"] +TENANT = "tenant1" +URL_PATH = str(TenantAPIs.TIMESERIES) +TIMESERIES_ID = "custom.test.timeseries" +RESPONSE_DIR = "tests/mockserver_payloads/responses/timeseries" + + +class TestGetTimeseries(unittest.TestCase): + """Test cases for fetch operations""" + + def test_get_metric_with_prediction(self): + """Test getting metric data with prediction""" + response_file = f"{RESPONSE_DIR}/get_predict.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + parameters={ + "timeseriesIdentifier": TIMESERIES_ID, + "relativeTime": "30mins", + "includeData": "True", + "predict": "True" + }, + response_file=response_file + ) + + result = timeseries.get_metric_data_with_prediction( + CLUSTER, TENANT, TIMESERIES_ID + ) + expected_result = testtools.expected_payload( + response_file + ).get("dataResult").get("dataPoints") + + self.assertEqual(result, expected_result) + + +if __name__ == "__main__": + unittest.main() From 078c3f9672ce5d04b9633f2ee0f3c9f92e672892 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Mon, 14 Dec 2020 11:09:00 +0000 Subject: [PATCH 161/204] PAF-75: fixed url path --- dynatrace/tenant/timeseries.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dynatrace/tenant/timeseries.py b/dynatrace/tenant/timeseries.py index c4b3ef2..3c39127 100644 --- a/dynatrace/tenant/timeseries.py +++ b/dynatrace/tenant/timeseries.py @@ -17,7 +17,6 @@ def get_metric_data_with_prediction(cluster, tenant, timeseries_id, **kwargs): \n @returns dict - predicted datapoints of the timeseries """ - kwargs["timeseriesIdentifier"] = timeseries_id kwargs["includeData"] = True kwargs["predict"] = True if not (("startTimestamp" in kwargs and "endTimestamp" in kwargs) @@ -27,7 +26,7 @@ def get_metric_data_with_prediction(cluster, tenant, timeseries_id, **kwargs): response = rh.make_api_call( cluster=cluster, tenant=tenant, - endpoint=ENDPOINT, + endpoint=f"{ENDPOINT}/{timeseries_id}", params=kwargs ).json() From a36e3ee254ab9593511a789b7357be78ad6f2168 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Mon, 14 Dec 2020 11:09:08 +0000 Subject: [PATCH 162/204] PAF-75: fixed url path --- tests/test_timeseries.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index efe3e21..97c096c 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -23,10 +23,9 @@ def test_get_metric_with_prediction(self): testtools.create_mockserver_expectation( cluster=CLUSTER, tenant=TENANT, - url_path=URL_PATH, + url_path=f"{URL_PATH}/{TIMESERIES_ID}", request_type=str(HTTP.GET), parameters={ - "timeseriesIdentifier": TIMESERIES_ID, "relativeTime": "30mins", "includeData": "True", "predict": "True" From 9ad0dfebf503319a3e48f89333e9acc1954339f7 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 08:12:11 +0000 Subject: [PATCH 163/204] PAF-74: Refurbished management_zones --- dynatrace/tenant/management_zones.py | 321 ++++++++++++++++++--------- 1 file changed, 219 insertions(+), 102 deletions(-) diff --git a/dynatrace/tenant/management_zones.py b/dynatrace/tenant/management_zones.py index dea1246..719adfd 100644 --- a/dynatrace/tenant/management_zones.py +++ b/dynatrace/tenant/management_zones.py @@ -1,122 +1,239 @@ -# This script's function is too add Management Zones -# for an application based on application and environment if provided -"""Management Zone Operations for Environment""" -import copy +"""Functions for Management Zone Operations via Configuration API""" + import json -from dynatrace.framework import request_handler as rh - -ENDPOINT = rh.TenantAPIs.MANAGEMENT_ZONES - - -def generate_mz_payload(application, env_zone=None): - """Create Payload for Management Zone based on Application and Environment""" - with open('../templates/mz_template.json', 'r') as mz_template: - mz_payload = json.load(mz_template) - - mz_payload['name'] = str(application) - # The Template will have - # Service Rules(0), Process Group Rules(1), Application Rules(2), - # Browser Monitors(3), HTTP Monitor(4), External Monitors(5), Manually Tagged Services (6), - # Manually Tagged Process Groups (7), Mobile Application (8), Custom Device Groups (9), - # Service and Process Groups are different because they allow Key/Value Pairs - - # TODO Consolidate by checking if Key/Value Pair exists - mz_payload['rules'][0]['conditions'][0]['comparisonInfo']['value']['value'] = str( - application) - mz_payload['rules'][1]['conditions'][0]['comparisonInfo']['value']['value'] = str( - application) - - for rule_num in range(2, 10): - mz_payload['rules'][rule_num]['conditions'][0]['comparisonInfo']['value']['key'] = \ - "APP: " + str(application) - - if env_zone: - # If environment exists, rename MZ and add environment conditions - mz_payload['name'] = str(application) + " - " + str(env_zone) - - # Service and Process Groups are different because they allow Key/Value Pairs - condition_payload = copy.deepcopy( - mz_payload['rules'][0]['conditions'][0]) - condition_payload['comparisonInfo']['value']['key'] = "ENV" - condition_payload['comparisonInfo']['value']['value'] = str(env_zone) - mz_payload['rules'][0]['conditions'].append(condition_payload) - - del condition_payload - condition_payload = copy.deepcopy( - mz_payload['rules'][1]['conditions'][0]) - condition_payload['comparisonInfo']['value']['key'] = "ENV" - condition_payload['comparisonInfo']['value']['value'] = str(env_zone) - mz_payload['rules'][1]['conditions'].append(condition_payload) - # Application, Browser Monitors, HTTP Monitor, External Monitors (in that order) - - for rule_num in range(2, 10): - del condition_payload - condition_payload = copy.deepcopy( - mz_payload['rules'][rule_num]['conditions'][0]) - condition_payload['comparisonInfo']['value']['key'] = "ENV: " + \ - str(env_zone) - mz_payload['rules'][rule_num]['conditions'].append( - condition_payload) - - return mz_payload - - -def add_management_zone(cluster, tenant, application, env_zone=None): - """Add Management Zone based on Application and Environment""" - mz_payload = generate_mz_payload(application, env_zone) - - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - method=rh.HTTP.POST, - endpoint=ENDPOINT, - json=mz_payload) +from enum import Enum, auto +from dynatrace.framework import request_handler as rh, logging + +ENDPOINT = str(rh.TenantAPIs.MANAGEMENT_ZONES) +logger = logging.get_logger(__name__) + + +class RuleTypes(Enum): + """Accepted values for Management Zone rule types.""" + APPMON_SERVER = auto() + APPMON_SYSTEM_PROFILE = auto() + AWS_ACCOUNT = auto() + AWS_APPLICATION_LOAD_BALANCER = auto() + AWS_AUTO_SCALING_GROUP = auto() + AWS_CLASSIC_LOAD_BALANCER = auto() + AWS_NETWORK_LOAD_BALANCER = auto() + AWS_RELATIONAL_DATABASE_SERVICE = auto() + AZURE = auto() + BROWSER_MONITOR = auto() + CLOUD_APPLICATION = auto() + CLOUD_APPLICATION_NAMESPACE = auto() + CLOUD_FOUNDRY_FOUNDATION = auto() + CUSTOM_APPLICATION = auto() + CUSTOM_DEVICE = auto() + CUSTOM_DEVICE_GROUP = auto() + DATA_CENTER_SERVICE = auto() + ENTERPRISE_APPLICATION = auto() + ESXI_HOST = auto() + EXTERNAL_MONITOR = auto() + HOST = auto() + HOST_GROUP = auto() + HTTP_MONITOR = auto() + KUBERNETES_CLUSTER = auto() + MOBILE_APPLICATION = auto() + OPENSTACK_ACCOUNT = auto() + PROCESS_GROUP = auto() + SERVICE = auto() + WEB_APPLICATION = auto() + + def __str__(self): + """Overriding default __str__ to return the name.""" + return self.name + + +def generate_mz_template(name, tags): + """Generates a standard Management Zone with custom name and rules matching tags. + The rules include Hosts, Services, Process Groups, Web & Mobile Applications, + Browser, HTTP, and External Synthetic Tests, and Custom Device Groups. + Tags must be given as a tuple in this order: context, key, value (optional). + \n + @param name (str) - The name of the Management Zone to be created\n + @param tags (list(Tuple)) - [0] is tag context, [1] is tag key, [2] is the tag value + \n + @returns dict - Management Zone + """ + logger.info("Building standard Management Zone from template") + logger.debug(f"Name: {name}; Tags: {tags}") + me_types = [ + RuleTypes.HOST, RuleTypes.SERVICE, RuleTypes.PROCESS_GROUP, + RuleTypes.WEB_APPLICATION, RuleTypes.BROWSER_MONITOR, RuleTypes.HTTP_MONITOR, + RuleTypes.MOBILE_APPLICATION, RuleTypes.CUSTOM_DEVICE_GROUP, + RuleTypes.EXTERNAL_MONITOR + ] + mz_rules = [ + dict( + type=str(me_type), + enabled=True, + propagationTypes=[], + conditions=[ + dict( + key=dict(attribute=f"{me_type}_TAGS"), + comparisonInfo=dict( + type="TAG", + operator="EQUALS" if len(tag) > 2 else "TAG_KEY_EQUALS", + value=dict( + context=tag[0], + key=tag[1], + value=tag[2] if len(tag) > 2 else None + ), + negate=False + ) + ) for tag in tags + ] + ) for me_type in me_types + ] + + mz_json = dict(name=name, rules=mz_rules) + + return mz_json + + +def add_management_zone(cluster, tenant, mz_json): + """Adds a new management zone to the tenant. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param mz_json (dict) - Management Zone definition, to be sent as JSON payload + \n + @returns str - ID of the newly created Management Zone, if successful + """ + logger.info(f"Adding a new Management Zone in tenant {tenant}") + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.POST, + endpoint=ENDPOINT, + json=mz_json + ) + if "id" in response.json(): - return (response.json())['id'] + return response.json()['id'] + return response.text -def change_management_zone(cluster, tenant, mz_id, application, env_zone=None): - """Add Management Zone based on Application and Environment""" - mz_payload = generate_mz_payload(application, env_zone) +def update_management_zone(cluster, tenant, mz_id, mz_json): + """Updates an existing Management Zone with given definition. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param mz_json (dict) - Management Zone definition, to be sent as JSON payload\n + @param mz_id (str) - ID of the Management Zone to update + \n + @returns Response - HTTP Response to the request + """ + logger.info(f"Updating Management Zone with ID {mz_id} in tenant {tenant}") - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - method=rh.HTTP.PUT, - endpoint=f"{ENDPOINT}/{mz_id}", - json=mz_payload) - print(response.status_code) + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.PUT, + endpoint=f"{ENDPOINT}/{mz_id}", + json=mz_json + ) + + return response def delete_management_zone_by_id(cluster, tenant, mz_id): - """Delete Management Zone by Management Zone ID""" - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - method=rh.HTTP.DELETE, - endpoint=f"{ENDPOINT}/{mz_id}") - print(response.status_code) + """Deletes an existing Management Zone, referenced by ID. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param mz_id (str) - ID of the Management Zone to delete + \n + @returns Response - HTTP Response to the request + """ + logger.info(f"Deleting Management Zone with ID {mz_id} from tenant {tenant}") + + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.DELETE, + endpoint=f"{ENDPOINT}/{mz_id}" + ) + + return response def delete_management_zone_by_name(cluster, tenant, mz_name): - """Delete Management Zone by Management Zone Name""" - # TODO This function - return "TODO " + cluster + tenant + mz_name - - -def get_management_zone_list(cluster, tenant): - """Get all Management Zones in Environment""" - # TODO Cache Management Zone list for Env, and add a cleanup script to remove after run. - response = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=ENDPOINT) - mz_list_raw = response.json() - return mz_list_raw['values'] + """Deletes an existing Management Zone, referenced by name. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param mz_name (str) - name of the Management Zone to delete + \n + @returns Response - HTTP Response to the request + \n + @throws RuntimeError - when Management Zone was not found in tenant + """ + mz_id = get_management_zone_id(cluster, tenant, mz_name) + + if not mz_id: + raise RuntimeError( + f"Error: No Management Zone found with name {mz_name} in tenant {tenant}" + ) + + logger.info(f"Deleting the Management Zone from tenant") + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + method=rh.HTTP.DELETE, + endpoint=f"{ENDPOINT}/{mz_id}" + ) + + return response + + +def get_all_management_zones(cluster, tenant): + """Gets all Management Zones within a Tenant. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set + \n + @returns list - list of Management Zones + """ + logger.info(f"Getting all Management Zones from tenant {tenant}") + management_zones = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT + ).json().get("values") + + return management_zones def get_management_zone_id(cluster, tenant, mz_name): - """Get Management Zone ID of Management Zone Name""" - mz_list = get_management_zone_list(cluster, tenant) + """Gets the ID of a Management Zone referenced by name. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param mz_name (str) - name of the Management Zone to find + \n + @returns str - ID of the Management Zone if found. None otherwise. + """ + logger.info(f"Finding ID for Management Zone with name {mz_name} in tenant {tenant}") + mz_list = get_all_management_zones(cluster, tenant) for m_zone in mz_list: if m_zone['name'] == mz_name: return m_zone['id'] return None + + +def import_mz_from_file(file): + """Reads a Management Zone definition from a (JSON) file. + \n + @param file (str) - the file to read (must be valid JSON) + \n + @returns dict - dictionary created from reading the file + """ + logger.info("Reading Management Zone from file.") + with open(file=file, mode="r") as f: + mz = json.load(f.read()) + + return mz From df3a2b1ce98b57d8258a9c3520474ff820eb3374 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 10:30:34 +0000 Subject: [PATCH 164/204] PAF-74: code improvements resulted from testing --- dynatrace/tenant/management_zones.py | 33 +++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/dynatrace/tenant/management_zones.py b/dynatrace/tenant/management_zones.py index 719adfd..ddcabd1 100644 --- a/dynatrace/tenant/management_zones.py +++ b/dynatrace/tenant/management_zones.py @@ -52,10 +52,18 @@ def generate_mz_template(name, tags): Tags must be given as a tuple in this order: context, key, value (optional). \n @param name (str) - The name of the Management Zone to be created\n - @param tags (list(Tuple)) - [0] is tag context, [1] is tag key, [2] is the tag value + @param tags (list(tuple)) - [0] is tag context, [1] is tag key, [2] is the tag value \n @returns dict - Management Zone """ + if not isinstance(tags, list): + raise ValueError( + f"Tags must be given as a list of tuples. Found {type(tags)} instead." + ) + if not all(isinstance(tag, tuple) for tag in tags): + raise ValueError( + "All provided tags must be tuples. Found a mix of types instead." + ) logger.info("Building standard Management Zone from template") logger.debug(f"Name: {name}; Tags: {tags}") me_types = [ @@ -178,7 +186,7 @@ def delete_management_zone_by_name(cluster, tenant, mz_name): f"Error: No Management Zone found with name {mz_name} in tenant {tenant}" ) - logger.info(f"Deleting the Management Zone from tenant") + logger.info("Deleting the Management Zone from tenant") response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -207,6 +215,25 @@ def get_all_management_zones(cluster, tenant): return management_zones +def get_management_zone_details(cluster, tenant, mz_id): + """Gets the full details of a Management Zone referenced by ID. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param mz_id (str) - ID of the Management Zone to fetch + \n + @returns dict - Management Zone details + """ + logger.info(f"Getting details for Management Zone with id {mz_id} in tenant {tenant}") + mz_details = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{mz_id}" + ).json() + + return mz_details + + def get_management_zone_id(cluster, tenant, mz_name): """Gets the ID of a Management Zone referenced by name. \n @@ -234,6 +261,6 @@ def import_mz_from_file(file): """ logger.info("Reading Management Zone from file.") with open(file=file, mode="r") as f: - mz = json.load(f.read()) + mz = json.load(f) return mz From a4527c5bbd2383edc11bebab3c0a3444b7f890e1 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 10:31:03 +0000 Subject: [PATCH 165/204] PAF-74: testing cases for management_zones --- .../requests/management_zones/mz.json | 203 +++++++++++++++ .../responses/management_zones/created.json | 3 + .../responses/management_zones/get_all.json | 16 ++ .../responses/management_zones/get_mz.json | 203 +++++++++++++++ tests/test_management_zones.py | 233 ++++++++++++++++++ 5 files changed, 658 insertions(+) create mode 100644 tests/mockserver_payloads/requests/management_zones/mz.json create mode 100644 tests/mockserver_payloads/responses/management_zones/created.json create mode 100644 tests/mockserver_payloads/responses/management_zones/get_all.json create mode 100644 tests/mockserver_payloads/responses/management_zones/get_mz.json create mode 100644 tests/test_management_zones.py diff --git a/tests/mockserver_payloads/requests/management_zones/mz.json b/tests/mockserver_payloads/requests/management_zones/mz.json new file mode 100644 index 0000000..729e259 --- /dev/null +++ b/tests/mockserver_payloads/requests/management_zones/mz.json @@ -0,0 +1,203 @@ +{ + "name": "Mock_MZ", + "rules": [ + { + "type": "HOST", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "HOST_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "SERVICE", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "SERVICE_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "PROCESS_GROUP", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "PROCESS_GROUP_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "WEB_APPLICATION", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "WEB_APPLICATION_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "BROWSER_MONITOR", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "BROWSER_MONITOR_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "HTTP_MONITOR", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "HTTP_MONITOR_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "MOBILE_APPLICATION", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "MOBILE_APPLICATION_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "CUSTOM_DEVICE_GROUP", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "CUSTOM_DEVICE_GROUP_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "EXTERNAL_MONITOR", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "EXTERNAL_MONITOR_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/management_zones/created.json b/tests/mockserver_payloads/responses/management_zones/created.json new file mode 100644 index 0000000..ae58097 --- /dev/null +++ b/tests/mockserver_payloads/responses/management_zones/created.json @@ -0,0 +1,3 @@ +{ + "id": "1234567890" +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/management_zones/get_all.json b/tests/mockserver_payloads/responses/management_zones/get_all.json new file mode 100644 index 0000000..b54f797 --- /dev/null +++ b/tests/mockserver_payloads/responses/management_zones/get_all.json @@ -0,0 +1,16 @@ +{ + "values": [ + { + "id": "1234566779789879", + "name": "Mock_MZ_1" + }, + { + "id": "1234567890", + "name": "Mock_MZ" + }, + { + "id": "9987342936473", + "name": "Mock_MZ_2" + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/management_zones/get_mz.json b/tests/mockserver_payloads/responses/management_zones/get_mz.json new file mode 100644 index 0000000..729e259 --- /dev/null +++ b/tests/mockserver_payloads/responses/management_zones/get_mz.json @@ -0,0 +1,203 @@ +{ + "name": "Mock_MZ", + "rules": [ + { + "type": "HOST", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "HOST_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "SERVICE", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "SERVICE_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "PROCESS_GROUP", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "PROCESS_GROUP_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "WEB_APPLICATION", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "WEB_APPLICATION_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "BROWSER_MONITOR", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "BROWSER_MONITOR_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "HTTP_MONITOR", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "HTTP_MONITOR_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "MOBILE_APPLICATION", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "MOBILE_APPLICATION_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "CUSTOM_DEVICE_GROUP", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "CUSTOM_DEVICE_GROUP_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + }, + { + "type": "EXTERNAL_MONITOR", + "enabled": true, + "propagationTypes": [], + "conditions": [ + { + "key": { + "attribute": "EXTERNAL_MONITOR_TAGS" + }, + "comparisonInfo": { + "type": "TAG", + "operator": "EQUALS", + "value": { + "context": "CONTEXTLESS", + "key": "Application", + "value": "DemoApp" + }, + "negate": false + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/test_management_zones.py b/tests/test_management_zones.py new file mode 100644 index 0000000..24f82bb --- /dev/null +++ b/tests/test_management_zones.py @@ -0,0 +1,233 @@ +"""Test Suite for Management Zone operations""" + +import unittest +import tests.tooling_for_test as testtools +from user_variables import FULL_SET +from dynatrace.tenant import management_zones +from dynatrace.framework.request_handler import TenantAPIs, HTTP + +CLUSTER = FULL_SET["mockserver1"] +TENANT = "tenant1" +URL_PATH = str(TenantAPIs.MANAGEMENT_ZONES) +RESPONSE_DIR = "tests/mockserver_payloads/responses/management_zones" +REQUEST_DIR = "tests/mockserver_payloads/requests/management_zones" +MZ_ID = "1234567890" +MZ_NAME = "Mock_MZ" +TAGS = [("CONTEXTLESS", "Application", "DemoApp")] + + +class TestUtils(unittest.TestCase): + """Test cases for utility functions, separate from API""" + + def test_rule_types_enum(self): + """Tests the RuleTypes enum. Must return object name""" + result = str(management_zones.RuleTypes.HTTP_MONITOR) + expected_result = management_zones.RuleTypes.HTTP_MONITOR.name + + self.assertEqual(result, expected_result) + + def test_generate_template(self): + """Tests generating a standard MZ template""" + mz_json_file = f"{RESPONSE_DIR}/get_mz.json" + + result = management_zones.generate_mz_template(MZ_NAME, TAGS) + expected_result = testtools.expected_payload(mz_json_file) + + self.assertEqual(result, expected_result) + + def test_import_mz_from_file(self): + """Tests importing a MZ JSON from file""" + mz_json_file = f"{RESPONSE_DIR}/get_mz.json" + + result = management_zones.import_mz_from_file(mz_json_file) + expected_result = testtools.expected_payload(mz_json_file) + + self.assertEqual(result, expected_result) + + +class TestFetchingMZs(unittest.TestCase): + """Test cases for fetching Management Zones and their details""" + + def test_get_all_management_zones(self): + """Tests fetching all the Managment Zones in the tenant""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = management_zones.get_all_management_zones(CLUSTER, TENANT) + expected_result = testtools.expected_payload(response_file).get("values") + + self.assertEqual(result, expected_result) + + def test_get_management_zone_details(self): + """Tests fetching the details of a management zone.""" + response_file = f"{RESPONSE_DIR}/get_mz.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{MZ_ID}", + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = management_zones.get_management_zone_details(CLUSTER, TENANT, MZ_ID) + expected_result = testtools.expected_payload(response_file) + + self.assertEqual(result, expected_result) + + def test_get_management_zone_id(self): + """Tests fetching the ID for a Management Zone referenced by name""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_file=response_file + ) + + result = management_zones.get_management_zone_id(CLUSTER, TENANT, MZ_NAME) + expected_result = MZ_ID + + self.assertEqual(result, expected_result) + + +class TestModifyingManagementZones(unittest.TestCase): + """Test cases for making changes to Management Zones within a tenant""" + + # BUG: Test tools needs fixing for using response.json() + # TODO: Uncomment once testtols is fixed. + # def test_add_management_zone(self): + # """Tests adding a new management zone to a tenant""" + # request_file = f"{REQUEST_DIR}/mz.json" + # response_file = f"{RESPONSE_DIR}/created.json" + # mz_json = testtools.expected_payload(request_file) + + # testtools.create_mockserver_expectation( + # cluster=CLUSTER, + # tenant=TENANT, + # url_path=URL_PATH, + # request_type=str(HTTP.POST), + # request_file=request_file, + # response_code=204, + # response_file=response_file + # ) + + # result = management_zones.add_management_zone(CLUSTER, TENANT, mz_json) + # expected_result = MZ_ID + + # self.assertEqual(result, expected_result) + + def test_update_management_zone(self): + """Tests updating an existing Management Zone in a tenant""" + request_file = f"{REQUEST_DIR}/mz.json" + mz_json = testtools.expected_payload(request_file) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{MZ_ID}", + request_type=str(HTTP.PUT), + request_file=request_file, + response_code=201 + ) + + result = management_zones.update_management_zone( + CLUSTER, TENANT, MZ_ID, mz_json + ).status_code + expected_result = 201 + + self.assertEqual(result, expected_result) + + def test_delete_management_zone_by_id(self): + """Tests deleting a Management Zone referenced by ID""" + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{MZ_ID}", + request_type=str(HTTP.DELETE), + response_code=204 + ) + + result = management_zones.delete_management_zone_by_id( + CLUSTER, TENANT, MZ_ID + ).status_code + expected_result = 204 + + self.assertEqual(result, expected_result) + + def test_delete_management_zone_by_name(self): + """Tests deleting a Management Zone referenced by name""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}", + request_type=str(HTTP.GET), + response_file=response_file, + mock_id="Req1" + ) + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{MZ_ID}", + request_type=str(HTTP.DELETE), + response_code=204, + mock_id="Req2" + ) + + result = management_zones.delete_management_zone_by_name( + CLUSTER, TENANT, MZ_NAME + ).status_code + expected_result = 204 + + self.assertEqual(result, expected_result) + + +class TestErrorHandling(unittest.TestCase): + """Test cases for error handling within Management Zone operations""" + + def test_generate_template_tags_not_list(self): + """Tests error handling when generating a standard MZ template. + Tags must be given as a list object. + """ + with self.assertRaises(ValueError): + management_zones.generate_mz_template(MZ_NAME, "wrong_tags") + + def test_generate_template_tags_not_tuples(self): + """Tests error handling when generating a standard MZ template. + Tags list must contain only tuples. + """ + with self.assertRaises(ValueError): + management_zones.generate_mz_template(MZ_NAME, ["wrong_tags"]) + + def test_delete_mz_not_found(self): + """Tests error handling when deleting a Management Zone by name. + Management Zone must return an ID. + """ + wrong_name = "non_existing_mz" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}", + request_type=str(HTTP.GET), + response_file=response_file + ) + + with self.assertRaises(RuntimeError): + management_zones.delete_management_zone_by_name(CLUSTER, TENANT, wrong_name) + + +if __name__ == "__main__": + unittest.main() From 163fc6fb8b0d58d279fd3cb803927fc3a933d843 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 11:16:29 +0000 Subject: [PATCH 166/204] PAF-74: Added missing test --- tests/test_management_zones.py | 42 ++++++++++++++++------------------ 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/tests/test_management_zones.py b/tests/test_management_zones.py index 24f82bb..ba837cd 100644 --- a/tests/test_management_zones.py +++ b/tests/test_management_zones.py @@ -103,28 +103,26 @@ def test_get_management_zone_id(self): class TestModifyingManagementZones(unittest.TestCase): """Test cases for making changes to Management Zones within a tenant""" - # BUG: Test tools needs fixing for using response.json() - # TODO: Uncomment once testtols is fixed. - # def test_add_management_zone(self): - # """Tests adding a new management zone to a tenant""" - # request_file = f"{REQUEST_DIR}/mz.json" - # response_file = f"{RESPONSE_DIR}/created.json" - # mz_json = testtools.expected_payload(request_file) - - # testtools.create_mockserver_expectation( - # cluster=CLUSTER, - # tenant=TENANT, - # url_path=URL_PATH, - # request_type=str(HTTP.POST), - # request_file=request_file, - # response_code=204, - # response_file=response_file - # ) - - # result = management_zones.add_management_zone(CLUSTER, TENANT, mz_json) - # expected_result = MZ_ID - - # self.assertEqual(result, expected_result) + def test_add_management_zone(self): + """Tests adding a new management zone to a tenant""" + request_file = f"{REQUEST_DIR}/mz.json" + response_file = f"{RESPONSE_DIR}/created.json" + mz_json = testtools.expected_payload(request_file) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.POST), + request_file=request_file, + response_code=201, + response_file=response_file + ) + + result = management_zones.add_management_zone(CLUSTER, TENANT, mz_json) + expected_result = MZ_ID + + self.assertEqual(result, expected_result) def test_update_management_zone(self): """Tests updating an existing Management Zone in a tenant""" From c5845812fc9abaa751b48a2f8db4b6547f99ef23 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 12:11:22 +0000 Subject: [PATCH 167/204] PAF-74: linting - use lazy logging and more pythonic var names --- dynatrace/tenant/management_zones.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/dynatrace/tenant/management_zones.py b/dynatrace/tenant/management_zones.py index ddcabd1..6e97f10 100644 --- a/dynatrace/tenant/management_zones.py +++ b/dynatrace/tenant/management_zones.py @@ -65,7 +65,7 @@ def generate_mz_template(name, tags): "All provided tags must be tuples. Found a mix of types instead." ) logger.info("Building standard Management Zone from template") - logger.debug(f"Name: {name}; Tags: {tags}") + logger.debug("Name: %s; Tags: %s" % (name, tags)) me_types = [ RuleTypes.HOST, RuleTypes.SERVICE, RuleTypes.PROCESS_GROUP, RuleTypes.WEB_APPLICATION, RuleTypes.BROWSER_MONITOR, RuleTypes.HTTP_MONITOR, @@ -109,7 +109,7 @@ def add_management_zone(cluster, tenant, mz_json): \n @returns str - ID of the newly created Management Zone, if successful """ - logger.info(f"Adding a new Management Zone in tenant {tenant}") + logger.info("Adding a new Management Zone in tenant %s" % tenant) response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -134,7 +134,7 @@ def update_management_zone(cluster, tenant, mz_id, mz_json): \n @returns Response - HTTP Response to the request """ - logger.info(f"Updating Management Zone with ID {mz_id} in tenant {tenant}") + logger.info("Updating Management Zone with ID %s in tenant %s" % (mz_id, tenant)) response = rh.make_api_call( cluster=cluster, @@ -156,7 +156,7 @@ def delete_management_zone_by_id(cluster, tenant, mz_id): \n @returns Response - HTTP Response to the request """ - logger.info(f"Deleting Management Zone with ID {mz_id} from tenant {tenant}") + logger.info("Deleting Management Zone with ID %s from tenant %s" % (mz_id, tenant)) response = rh.make_api_call( cluster=cluster, @@ -183,7 +183,7 @@ def delete_management_zone_by_name(cluster, tenant, mz_name): if not mz_id: raise RuntimeError( - f"Error: No Management Zone found with name {mz_name} in tenant {tenant}" + "Error: No Management Zone found with name %s in tenant %s" % (mz_id, tenant) ) logger.info("Deleting the Management Zone from tenant") @@ -205,7 +205,7 @@ def get_all_management_zones(cluster, tenant): \n @returns list - list of Management Zones """ - logger.info(f"Getting all Management Zones from tenant {tenant}") + logger.info("Getting all Management Zones from tenant %s" % tenant) management_zones = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -224,7 +224,9 @@ def get_management_zone_details(cluster, tenant, mz_id): \n @returns dict - Management Zone details """ - logger.info(f"Getting details for Management Zone with id {mz_id} in tenant {tenant}") + logger.info( + "Getting details for Management Zone with id %s in tenant %s" % (mz_id, tenant) + ) mz_details = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -243,7 +245,9 @@ def get_management_zone_id(cluster, tenant, mz_name): \n @returns str - ID of the Management Zone if found. None otherwise. """ - logger.info(f"Finding ID for Management Zone with name {mz_name} in tenant {tenant}") + logger.info( + "Finding ID for Management Zone with name %s in tenant %s" % (mz_name, tenant) + ) mz_list = get_all_management_zones(cluster, tenant) for m_zone in mz_list: @@ -260,7 +264,7 @@ def import_mz_from_file(file): @returns dict - dictionary created from reading the file """ logger.info("Reading Management Zone from file.") - with open(file=file, mode="r") as f: - mz = json.load(f) + with open(file=file, mode="r") as json_file: + mz_details = json.load(json_file) - return mz + return mz_details From 4a3d7699dd415e470b03c85bf9d42631c144e85a Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 12:36:54 +0000 Subject: [PATCH 168/204] PAF-74: logging done right --- dynatrace/tenant/management_zones.py | 44 ++++++++++++++++------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/dynatrace/tenant/management_zones.py b/dynatrace/tenant/management_zones.py index 6e97f10..ecc9534 100644 --- a/dynatrace/tenant/management_zones.py +++ b/dynatrace/tenant/management_zones.py @@ -56,16 +56,20 @@ def generate_mz_template(name, tags): \n @returns dict - Management Zone """ - if not isinstance(tags, list): - raise ValueError( - f"Tags must be given as a list of tuples. Found {type(tags)} instead." - ) - if not all(isinstance(tag, tuple) for tag in tags): - raise ValueError( - "All provided tags must be tuples. Found a mix of types instead." - ) + try: + if not isinstance(tags, list): + raise ValueError( + f"Tags must be given as a list of tuples. Found {type(tags)} instead." + ) + if not all(isinstance(tag, tuple) for tag in tags): + raise ValueError( + "All provided tags must be tuples. Found a mix of types instead." + ) + except ValueError: + logger.exception("Error: invalid format for tags object.", stack_info=True) + raise logger.info("Building standard Management Zone from template") - logger.debug("Name: %s; Tags: %s" % (name, tags)) + logger.debug("Name: %s; Tags: %s", name, tags) me_types = [ RuleTypes.HOST, RuleTypes.SERVICE, RuleTypes.PROCESS_GROUP, RuleTypes.WEB_APPLICATION, RuleTypes.BROWSER_MONITOR, RuleTypes.HTTP_MONITOR, @@ -109,7 +113,7 @@ def add_management_zone(cluster, tenant, mz_json): \n @returns str - ID of the newly created Management Zone, if successful """ - logger.info("Adding a new Management Zone in tenant %s" % tenant) + logger.info("Adding a new Management Zone in tenant %s", tenant) response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -134,7 +138,7 @@ def update_management_zone(cluster, tenant, mz_id, mz_json): \n @returns Response - HTTP Response to the request """ - logger.info("Updating Management Zone with ID %s in tenant %s" % (mz_id, tenant)) + logger.info("Updating Management Zone with ID %s in tenant %s", mz_id, tenant) response = rh.make_api_call( cluster=cluster, @@ -156,7 +160,7 @@ def delete_management_zone_by_id(cluster, tenant, mz_id): \n @returns Response - HTTP Response to the request """ - logger.info("Deleting Management Zone with ID %s from tenant %s" % (mz_id, tenant)) + logger.info("Deleting Management Zone with ID %s from tenant %s", mz_id, tenant) response = rh.make_api_call( cluster=cluster, @@ -182,9 +186,13 @@ def delete_management_zone_by_name(cluster, tenant, mz_name): mz_id = get_management_zone_id(cluster, tenant, mz_name) if not mz_id: - raise RuntimeError( - "Error: No Management Zone found with name %s in tenant %s" % (mz_id, tenant) - ) + try: + raise RuntimeError( + f"Error: No Management Zone found with name {mz_id} in tenant {tenant}" + ) + except RuntimeError: + logger.exception("Error: Management Zone not found.", stack_info=True) + raise logger.info("Deleting the Management Zone from tenant") response = rh.make_api_call( @@ -205,7 +213,7 @@ def get_all_management_zones(cluster, tenant): \n @returns list - list of Management Zones """ - logger.info("Getting all Management Zones from tenant %s" % tenant) + logger.info("Getting all Management Zones from tenant %s", tenant) management_zones = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -225,7 +233,7 @@ def get_management_zone_details(cluster, tenant, mz_id): @returns dict - Management Zone details """ logger.info( - "Getting details for Management Zone with id %s in tenant %s" % (mz_id, tenant) + "Getting details for Management Zone with id %s in tenant %s", mz_id, tenant ) mz_details = rh.make_api_call( cluster=cluster, @@ -246,7 +254,7 @@ def get_management_zone_id(cluster, tenant, mz_name): @returns str - ID of the Management Zone if found. None otherwise. """ logger.info( - "Finding ID for Management Zone with name %s in tenant %s" % (mz_name, tenant) + "Finding ID for Management Zone with name %s in tenant %s", mz_name, tenant ) mz_list = get_all_management_zones(cluster, tenant) From 09dbeceaea0136e7048384eccf3e8b11a79a307e Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 12:45:42 +0000 Subject: [PATCH 169/204] PAF-74: linting - __str__ returns str now --- dynatrace/tenant/management_zones.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/tenant/management_zones.py b/dynatrace/tenant/management_zones.py index ecc9534..24d78bd 100644 --- a/dynatrace/tenant/management_zones.py +++ b/dynatrace/tenant/management_zones.py @@ -42,7 +42,7 @@ class RuleTypes(Enum): def __str__(self): """Overriding default __str__ to return the name.""" - return self.name + return str(self.name) def generate_mz_template(name, tags): From ddd37ce0a1ebf3a8bb12d0e8c34b9e93511057c2 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 13:17:08 +0000 Subject: [PATCH 170/204] PAF-75: implemented lazy logging everywhere --- dynatrace/tenant/entities.py | 30 +++++++++++++++++------------- dynatrace/tenant/extensions.py | 26 +++++++++++++------------- dynatrace/tenant/metrics.py | 2 +- dynatrace/tenant/oneagents.py | 8 ++++---- dynatrace/tenant/problems.py | 20 ++++++++++---------- 5 files changed, 45 insertions(+), 41 deletions(-) diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index 964d013..0353768 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -131,7 +131,7 @@ def get_entities_tenantwide(cluster, tenant, entity_type, **kwargs): else: kwargs['entitySelector'] = f'type({entity_type})' - logger.info(f"Getting whole result set for entities in {tenant} tenant") + logger.info("Getting whole result set for entities in %s tenant", tenant) response = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -222,7 +222,7 @@ def get_entities_by_page(cluster, tenant, entity_type, **kwargs): else: kwargs['entitySelector'] = f'type({entity_type})' - logger.info(f"Getting paged result set for entities in {tenant} tenant") + logger.info("Getting paged result set for entities in %s tenant", tenant) response = rh.get_results_by_page( cluster=cluster, tenant=tenant, @@ -255,7 +255,7 @@ def get_entity(cluster, tenant, entity_id, **kwargs): else: kwargs['entitySelector'] = f'entityId({entity_id})' - logger.info(f"Getting entity details for ID(s) {entity_id}") + logger.info("Getting entity details for ID(s) %s", entity_id) response = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -295,7 +295,7 @@ def get_entity_count_tenantwide(cluster, tenant, entity_type, **kwargs): else: kwargs['entitySelector'] = f'type({entity_type})' - logger.info(f"Getting entity count from {tenant} tenant") + logger.info("Getting entity count from %s tenant", tenant) response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -369,20 +369,22 @@ def add_tags(cluster, tenant, tag_list, **kwargs): try: raise TypeError("No tags provided") except TypeError: - logger.exception("Error: No tags provided") + logger.exception("Error: No tags provided", stack_info=True) raise if not isinstance(tag_list, list): try: raise TypeError("tags_list is not a list") except TypeError: - logger.exception("Error: tags_list must be a list") + logger.exception("Error: tags_list must be a list", stack_info=True) raise if 'type' not in kwargs['entitySelector'] \ and 'entityId' not in kwargs['entitySelector']: try: raise ValueError("entitySelector must have at least type or entityId") except ValueError: - logger.exception("Error: entitySelector missing required values") + logger.exception( + "Error: entitySelector missing required values", stack_info=True + ) raise logger.info("Adding tags to entities") @@ -417,26 +419,28 @@ def delete_tag(cluster, tenant, tag_key, tag_value=None, **kwargs): try: raise TypeError("No tag key provided") except TypeError: - logger.exception("Error: Must provide a tag key") + logger.exception("Error: Must provide a tag key", stack_info=True) raise if 'type' not in kwargs['entitySelector'] \ and 'entityId' not in kwargs['entitySelector']: try: raise ValueError("entitySelector must have at least type or entityId") except ValueError: - logger.exception("Error: entitySelector missing required values") + logger.exception( + "Error: entitySelector missing required values", stack_info=True + ) raise # Set params for tag key & value kwargs['key'] = tag_key if tag_value == "all": kwargs['deleteAllWithKey'] = True - logger.info(f"Deleting all {tag_key} tags from entities") + logger.info("Deleting all %s tags from entities", tag_key) elif tag_value: kwargs['value'] = tag_value - logger.info(f"Deleting {tag_key}:{tag_value} tags from entities") + logger.info("Deleting %s:%s tags from entities", tag_key, tag_value) else: - logger.info(f"Deleting {tag_key} tag from entities.") + logger.info("Deleting %s tag from entities.", tag_key) response = rh.make_api_call( cluster=cluster, @@ -475,7 +479,7 @@ def custom_device(cluster, tenant, json_data): and not json_data.get('type'): raise ValueError("type must be in JSON data when creating a device") except ValueError: - logger.exception("Error: Missing mandatory details.") + logger.exception("Error: Missing mandatory details.", stack_info=True) raise logger.info("Creating/updating custom device.") diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index 78afd5e..e32d9cf 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -17,7 +17,7 @@ def get_all_extensions(cluster, tenant, page_size=200): \n @returns list - list of extensions """ - logger.info(f"Getting all extensions in {tenant} tenant") + logger.info("Getting all extensions in %s tenant", tenant) extension_list = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -39,7 +39,7 @@ def get_extension_details(cluster, tenant, extension_id): \n @returns (dict) - JSON response containing extension details """ - logger.info(f"Getting extension details for {extension_id}") + logger.info("Getting extension details for %s", extension_id) details = rh.make_api_call( cluster=cluster, endpoint=f"{ENDPOINT}/{extension_id}", @@ -58,7 +58,7 @@ def get_extension_metrics(cluster, tenant, extension_id): \n @returns list - list of metric IDs """ - logger.info(f"Getting metrics collected by extension {extension_id}") + logger.info("Getting metrics collected by extension %s", extension_id) metric_group = get_extension_details(cluster, tenant, extension_id).get('metricGroup') ext_metrics = metrics.get_metric_descriptor( cluster=cluster, @@ -79,7 +79,7 @@ def get_extension_global_config(cluster, tenant, extension_id): \n @returns dict - global configuration """ - logger.info(f"Getting global configuration for extension {extension_id}") + logger.info("Getting global configuration for extension %s", extension_id) config = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -101,7 +101,7 @@ def get_extension_instance_config(cluster, tenant, extension_id, instance_id): @returns dict - instance configuration """ logger.info( - f"Getting configuration for instance {instance_id} on extension {extension_id}" + "Getting configuration for instance %s on extension %s", instance_id, extension_id ) config = rh.make_api_call( cluster=cluster, @@ -123,7 +123,7 @@ def get_extension_states(cluster, tenant, extension_id): \n @returns list - states/instances of this extension """ - logger.info(f"Getting states for extension {extension_id}") + logger.info("Getting states for extension %s", extension_id) states = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -145,7 +145,7 @@ def get_extension_instances(cluster, tenant, extension_id): \n @returns list - configuration instances for this extension """ - logger.info(f"Getting instances for extension {extension_id}") + logger.info("Getting instances for extension %s", extension_id) instances = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -170,7 +170,7 @@ def enable_global_config(cluster, tenant, extension_id): config = get_extension_global_config(cluster, tenant, extension_id) config['enabled'] = True - logger.info(f"Enabling global config for extension {extension_id}") + logger.info("Enabling global config for extension %s", extension_id) response = update_global_config(cluster, tenant, extension_id, config) return response @@ -189,7 +189,7 @@ def disable_global_config(cluster, tenant, extension_id): config = get_extension_global_config(cluster, tenant, extension_id) config['enabled'] = False - logger.info(f"Disabling global config for extension {extension_id}") + logger.info("Disabling global config for extension %s", extension_id) response = update_global_config(cluster, tenant, extension_id, config) return response @@ -206,7 +206,7 @@ def update_global_config(cluster, tenant, extension_id, config): \n @returns dict - HTTP response to request """ - logger.info(f"Updating global config for extension {extension_id}") + logger.info("Updating global config for extension %s", extension_id) response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -243,7 +243,7 @@ def enable_instance_config(cluster, tenant, extension_id, instance_id): if 'activeGate' in config: config['extensionId'] = extension_id - logger.info(f"Enabling config for instance {instance_id} of {extension_id}") + logger.info("Enabling config for instance %s of %s", instance_id, extension_id) response = update_instance_config( cluster, tenant, extension_id, instance_id, config ) @@ -276,7 +276,7 @@ def disable_instance_config(cluster, tenant, extension_id, instance_id): if 'activeGate' in config: config['extensionId'] = extension_id - logger.info(f"Disabling config for instance {instance_id} of {extension_id}") + logger.info("Disabling config for instance %s of %s", instance_id, extension_id) response = update_instance_config( cluster, tenant, extension_id, instance_id, config ) @@ -296,7 +296,7 @@ def update_instance_config(cluster, tenant, extension_id, instance_id, config): \n @returns dict - HTTP response to request """ - logger.info(f"Updating config for instance {instance_id} of {extension_id}") + logger.info("Updating config for instance %s of %s", instance_id, extension_id) response = rh.make_api_call( cluster=cluster, tenant=tenant, diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index 925d853..52d75a3 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -82,7 +82,7 @@ def get_metric_data(cluster, tenant, **kwargs): if 'metric key that could not be resolved in the metric registry' in str(err): logger.warn("Invalid metric ID encountered. Returning results so far.") break - logger.exception("Error: Invalid API response") + logger.exception("Error: Invalid API response", stack_info=True) raise for result in response.json().get('result'): diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index 5ebdb98..c66d4d0 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -15,7 +15,7 @@ def get_host_units_tenantwide(cluster, tenant, **kwargs): """ host_units = 0 - logger.info(f"Getting hosts from tenant {tenant}") + logger.info("Getting hosts from tenant %s", tenant) host_list = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -41,7 +41,7 @@ def get_host_units_clusterwide(cluster, aggregated=True, **kwargs): with tenants as keys if not aggregated. """ total_host_units = 0 - host_units = {} + host_units = %s logger.info("Getting host units for the whole cluster") for tenant in cluster['tenant']: @@ -66,7 +66,7 @@ def get_host_units_setwide(full_set, aggregated=True, **kwargs): with clusters as keys if not aggregated. """ total_host_units = 0 - host_units = {} + host_units = %s logger.info("Getting host units for the whole set") for cluster in full_set: @@ -89,7 +89,7 @@ def get_oneagents_tenantwide(cluster, tenant, **kwargs): @returns - list of OneAgents """ - logger.info(f"Getting OneAgents from tenant {tenant}") + logger.info("Getting OneAgents from tenant %s", tenant) return rh.get_results_whole( cluster=cluster, tenant=tenant, diff --git a/dynatrace/tenant/problems.py b/dynatrace/tenant/problems.py index d204fc6..bc78745 100644 --- a/dynatrace/tenant/problems.py +++ b/dynatrace/tenant/problems.py @@ -13,7 +13,7 @@ def get_all_problems(cluster, tenant, **kwargs): \n @returns list - list of problems """ - logger.info(f"Getting problems from tenant {tenant}") + logger.info("Getting problems from tenant %s", tenant) problems_list = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -34,7 +34,7 @@ def get_problem_count(cluster, tenant, **kwargs): \n @returns int - number of problems """ - logger.info(f"Getting the total problem count in tenant {tenant}") + logger.info("Getting the total problem count in tenant %s", tenant) problems_list = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -58,7 +58,7 @@ def get_problem_details(cluster, tenant, problem_id, **kwargs): \n @returns (dict) - problem details """ - logger.info(f"Getting problem details for problem {problem_id}") + logger.info("Getting problem details for problem %s", problem_id) details = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -79,8 +79,8 @@ def close_problem(cluster, tenant, problem_id, comment=""): \n @returns Response - HTTP response for the request """ - logger.info(f"Closing problem {problem_id}") - logger.info(f"Closing comment: {comment}") + logger.info("Closing problem %s", problem_id) + logger.info("Closing comment: %s", comment) response = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -103,7 +103,7 @@ def get_all_comments(cluster, tenant, problem_id, **kwargs): \n @returns list - list of comments """ - logger.info(f"Getting comments from problem {problem_id}") + logger.info("Getting comments from problem %s", problem_id) comments = rh.get_results_whole( cluster=cluster, tenant=tenant, @@ -126,7 +126,7 @@ def get_comment(cluster, tenant, problem_id, comment_id): \n @returns dict - comment details """ - logger.info(f"Getting details for comment {comment_id} from problem {problem_id}") + logger.info("Getting details for comment %s from problem %s", comment_id, problem_id) comment = rh.make_api_call( cluster=cluster, tenant=tenant, @@ -148,7 +148,7 @@ def add_comment(cluster, tenant, problem_id, **kwargs): \n @returns Response - HTTP response for the request """ - logger.info(f"Adding comment to problem {problem_id}") + logger.info("Adding comment to problem %s", problem_id) comment = kwargs.get("comment") if "comment" in kwargs else "" context = kwargs.get("context") if "context" in kwargs else "" @@ -176,7 +176,7 @@ def update_comment(cluster, tenant, problem_id, comment_id, **kwargs): \n @returns Response - HTTP response for the request """ - logger.info(f"Updating comment {comment_id} from problem {problem_id}") + logger.info("Updating comment %s from problem %s", comment_id, problem_id) comment = get_comment(cluster, tenant, problem_id, comment_id) if "comment" in kwargs: @@ -205,7 +205,7 @@ def delete_comment(cluster, tenant, problem_id, comment_id): \n @returns Response - HTTP response for the request """ - logger.info(f"Deleting comment {comment_id} from problem {problem_id}") + logger.info("Deleting comment %s from problem %s", comment_id, problem_id) response = rh.make_api_call( cluster=cluster, tenant=tenant, From 4d80786f5c8a321d5a3f94b873a124fbf8fec421 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 13:17:49 +0000 Subject: [PATCH 171/204] PAF-79: lazy logging + typo fix --- dynatrace/framework/logging.py | 2 +- dynatrace/framework/request_handler.py | 29 ++++++++++++++------------ 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/dynatrace/framework/logging.py b/dynatrace/framework/logging.py index 50052b0..09f1015 100644 --- a/dynatrace/framework/logging.py +++ b/dynatrace/framework/logging.py @@ -36,7 +36,7 @@ def get_logger(name, filename="Framework.log"): "Could not setup logging - missing folder from settings." ) if not os.path.exists(folder): - os.mkedirs(folder) + os.makedirs(folder) file_handler = handlers.RotatingFileHandler( filename=f"{folder}/{filename}", delay=True, diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index e2cbffd..3bbc218 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -141,7 +141,7 @@ def make_api_call(cluster, endpoint, tenant=None, method=HTTP.GET, **kwargs): # Set the right URL for the operation url = f"{generate_tenant_url(cluster, tenant)}{endpoint}" \ if tenant else f"{HTTPS_STR}{cluster['url']}{endpoint}" - logger.debug(f"URL used for API call: {url}") + logger.debug("URL used for API call: %s", url) # Get correct token for the operation if 'onpremise' in str(endpoint) or 'cluster' in str(endpoint): @@ -193,11 +193,11 @@ def get_results_whole(cluster, tenant, endpoint, api_version, **kwargs): try: raise ValueError("For V2 APIs you must provide collected item.") except ValueError: - logger.exception("Error: item missing from V2 API call.") + logger.exception("Error: item missing from V2 API call.", stack_info=True) raise item = kwargs['item'] results = {} - logger.debug(f"Using V2 pagination for API to collect {item}") + logger.debug("Using V2 pagination for API to collect %s", item) else: is_v2 = False results = [] @@ -205,7 +205,7 @@ def get_results_whole(cluster, tenant, endpoint, api_version, **kwargs): while cursor: if cursor != 1: - logger.debug(f"Getting next page of results. Cursor is {cursor}") + logger.debug("Getting next page of results. Cursor is %", cursor) if not is_v2 or endpoint == TenantAPIs.ONEAGENTS: # V1 and OneAgents require all other query params are preserved kwargs['nextPageKey'] = cursor @@ -257,17 +257,17 @@ def get_results_by_page(cluster, tenant, endpoint, api_version, **kwargs): try: raise ValueError("For is_v2 APIs you must provide collected item.") except ValueError: - logger.exception("Error: item missing from V2 API call.") + logger.exception("Error: item missing from V2 API call.", stack_info=True) raise item = kwargs['item'] - logger.debug(f"Using V2 pagination for API to collect {item}") + logger.debug("Using V2 pagination for API to collect %s", item) else: logger.debug("Using V1 pagination for API") is_v2 = False while cursor: if cursor != 1: - logger.debug(f"Getting next page of results. Cursor is {cursor}") + logger.debug("Getting next page of results. Cursor is %s", cursor) # V1 requires all other query params are preserved if not is_v2 or endpoint == TenantAPIs.ONEAGENTS: kwargs['nextPageKey'] = cursor @@ -299,18 +299,19 @@ def check_response(response): ''' logger.debug("Validating the response for the API call.") - logger.debug(f"Response: {response}") + logger.debug("Response: %s", response) headers = response.headers if response.status_code == 429: - logger.warn("Endpoint request limit of " - f"{headers['x-ratelimit-limit']} was reached!") + logger.warn( + "Endpoint request limit of %s was reached!", headers['x-ratelimit-limit'] + ) # Wait until the limit resets and try again time_to_wait = int(headers['x-ratelimit-reset'])/1000000 - time.time() # Check that there's actually time to wait if time_to_wait > 0: - logger.warn(f"Waiting {time_to_wait} sec until the limit resets.") + logger.warn("Waiting %s sec until the limit resets.", time_to_wait) time.sleep(float(time_to_wait)) return False @@ -321,7 +322,7 @@ def check_response(response): f"{response.url}\n{response.status_code}\n{response.text}" ) except InvalidAPIResponseException: - logger.exception("Error: Invalid API response.") + logger.exception("Error: Invalid API response.", stack_info=True) raise return True @@ -334,7 +335,9 @@ def check_managed(cluster): try: raise ManagedClusterOnlyException() except ManagedClusterOnlyException: - logger.exception("Error: Managed operation attempted on SaaS cluster.") + logger.exception( + "Error: Managed operation attempted on SaaS cluster.", stack_info=True + ) raise From 3447c127b6c4e90842bd745519a9e039592c23b3 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 13:21:23 +0000 Subject: [PATCH 172/204] PAF-79: fixed typo that broke tests --- dynatrace/tenant/oneagents.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index c66d4d0..d889f5e 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -41,7 +41,7 @@ def get_host_units_clusterwide(cluster, aggregated=True, **kwargs): with tenants as keys if not aggregated. """ total_host_units = 0 - host_units = %s + host_units = {} logger.info("Getting host units for the whole cluster") for tenant in cluster['tenant']: @@ -66,7 +66,7 @@ def get_host_units_setwide(full_set, aggregated=True, **kwargs): with clusters as keys if not aggregated. """ total_host_units = 0 - host_units = %s + host_units = {} logger.info("Getting host units for the whole set") for cluster in full_set: From de25a65e19466212e5d596895bf427e9f5c67aeb Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 18:13:58 +0000 Subject: [PATCH 173/204] PAF-73: Refurbished request_attributes --- dynatrace/tenant/request_attributes.py | 250 +++++++++++++++++-------- 1 file changed, 175 insertions(+), 75 deletions(-) diff --git a/dynatrace/tenant/request_attributes.py b/dynatrace/tenant/request_attributes.py index 8071dcb..8714d28 100644 --- a/dynatrace/tenant/request_attributes.py +++ b/dynatrace/tenant/request_attributes.py @@ -1,75 +1,175 @@ -#!/bin/python3 -"""Request Attributes Operations""" -import json -from dynatrace.framework import request_handler as rh - -ENDPOINT = rh.TenantAPIs.REQUEST_ATTRIBUTES - - -def pull_to_files(cluster, tenant, ignore_disabled=True): - """Pull files from an environment to local""" - # API Calls needed: Pull RA, take the ID and pull the details of each RA - all_ra_call = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=ENDPOINT) - all_ra_json = all_ra_call.json() - all_ra_json = all_ra_json['values'] - # print (json.dumps(all_ra_json, indent=2)) - ra_file_list = [] - for request_attribute in all_ra_json: - single_ra_call = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=f"{ENDPOINT}/{request_attribute['id']}") - if single_ra_call.status_code == 200: - single_ra_json = single_ra_call.json() - if single_ra_json['enabled'] and ignore_disabled: - single_ra_json.pop("metadata") - single_ra_json.pop("id") - ra_file_name = "jsons/request_attributes/" + \ - str(single_ra_json['name']) + ".json" - with open(ra_file_name, 'w') as current_file: - json.dump(single_ra_json, current_file, indent=2) - ra_file_list.append(ra_file_name) - else: - print(single_ra_call.status_code) - return ra_file_list - - -def push_from_files(file_list, cluster, tenant): - """Push Request Attributes in JSONs to a tenant""" - - # Checks for Existing RAs to update them put request rather than a post that would fail - existing_ra_get = rh.make_api_call(cluster=cluster, tenant=tenant, endpoint=ENDPOINT) - existing_ra_json = existing_ra_get.json() - existing_ra_json = existing_ra_json['values'] - existing_ra_list = {} - for existing_ra in existing_ra_json: - existing_ra_list["jsons/request_attributes/" + - str(existing_ra['name']) + ".json"] = existing_ra['id'] - - for file in file_list: - with open(file, 'r') as ra_file: - ra_json = json.load(ra_file) - if file in existing_ra_list: - single_ra_post = rh.make_api_call( - cluster=cluster, - tenant=tenant, - method=rh.HTTP.PUT, - endpoint=f"{ENDPOINT}/{existing_ra_list[file]}", - json=ra_json - ) - else: - single_ra_post = rh.make_api_call( - cluster=cluster, - tenant=tenant, - method=rh.HTTP.POST, - endpoint=ENDPOINT, - json=ra_json - ) - if single_ra_post.status_code >= 400: - # NOTE: what about the check response in req handler!? - # That will throw an exception first, which this should except - print("Error with " + file + ". Status Code: " + - str(single_ra_post.status_code)) - else: - print("Success " + file + " " + single_ra_post.text) +"""Module for Request Attributes Operations via Configuration API""" + +from dynatrace.framework import request_handler as rh, logging + +ENDPOINT = str(rh.TenantAPIs.REQUEST_ATTRIBUTES) +logger = logging.get_logger(__name__) + + +def get_all_request_attributes(cluster, tenant): + """Get all request attributes within a tenant. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set + \n + @returns list - list of Request Attributes from tenant + """ + logger.info("Getting all request attributes in tenant %s", tenant) + request_attributes = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT + ).json() + + return request_attributes + + +def get_request_attribute_details(cluster, tenant, ra_id): + """Get the full details of a request attribute in the tenant. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param ra_id (str) - ID of the request attribute to fetch + \n + @returns dict - Request Attribute details + """ + logger.info( + "Getting details for request attribute with id %s in tenant %s", ra_id, tenant + ) + details = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{ra_id}" + ).json() + + return details + + +def create_request_attribute(cluster, tenant, ra_json): + """Creates a new request attribute from given JSON and adds it to the tenant. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param ra_json (dict) - details of the request attribute to be sent as JSON payload + \n + @returns Response - HTTP Response for the request + """ + logger.info( + "Adding a request attribute called %s in tenant %s", ra_json.get("name"), tenant + ) + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT, + method=rh.HTTP.POST, + json=ra_json + ) + + return response + + +def update_request_attribute(cluster, tenant, ra_id, ra_json): + """Updates an existing request attribute in the tenant. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param ra_json (dict) - details of the request attribute to be sent as JSON payload + \n + @returns Response - HTTP Response for the request + """ + logger.info( + "Updating request attribute with ID %s in tenant %s", ra_id, tenant + ) + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{ra_id}", + method=rh.HTTP.PUT, + json=ra_json + ) + + return response + + +def delete_request_attribute_by_id(cluster, tenant, ra_id): + """Deletes an existing request attribute, referenced by ID. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param ra_id (str) - ID of the request attribute to delete + \n + @returns Response - HTTP Response + """ + logger.info( + "Deleting request attribute with ID %s from tenant %s", ra_id, tenant + ) + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{ra_id}", + method=rh.HTTP.DELETE + ) + + return response + + +def delete_request_attribute_by_name(cluster, tenant, ra_name): + """Deletes an existing request attribute, referenced by name. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param ra_name (str) - name of the request attribute to delete + \n + @returns Response - HTTP Response + \n + @throws RuntimeError - when no ID was found for the request attribute + """ + ra_id = get_request_attribute_id(cluster, tenant, ra_name) + + if not ra_id: + try: + raise RuntimeError( + f"Error: request attribute with name {ra_name} was not found" + ) + except RuntimeError: + logger.exception("Error: request attribute not found.", stack_info=True) + raise + + return delete_request_attribute_by_id(cluster, tenant, ra_id) + + +def create_or_update_request_attribute(cluster, tenant, ra_json): + """Either creates a new request attribute from the provided JSON or updates it if it + already exists in the tenant. Either way, the request attribute will be in the + tenant. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param ra_json (dict) - details of the request attribute to be sent as JSON payload + \n + @returns Response - HTTP Response to the request + """ + ra_id = get_request_attribute_id(cluster, tenant, ra_json.get("name")) + + if ra_id: + return update_request_attribute(cluster, tenant, ra_id, ra_json) + + return create_request_attribute(cluster, tenant, ra_json) + + +def get_request_attribute_id(cluster, tenant, name): + """Gets the ID for a request attribute referenced by name. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param name (str) - name of the Request Attribute + \n + @returns str - ID of the request attribute if found. None otherwise. + """ + logger.info("Finding the ID for request attribute with name %s", name) + request_attributes = get_all_request_attributes(cluster, tenant) + + for req_attr in request_attributes: + if req_attr.get("name") == name: + return req_attr.get("id") + return None From 120aaefb0d0581785c444d8e7cae51709db98d92 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Tue, 15 Dec 2020 18:34:44 +0000 Subject: [PATCH 174/204] PAF-73: added exporting capability --- dynatrace/tenant/request_attributes.py | 42 +++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/dynatrace/tenant/request_attributes.py b/dynatrace/tenant/request_attributes.py index 8714d28..71ac3b9 100644 --- a/dynatrace/tenant/request_attributes.py +++ b/dynatrace/tenant/request_attributes.py @@ -1,5 +1,7 @@ """Module for Request Attributes Operations via Configuration API""" +import os +import json from dynatrace.framework import request_handler as rh, logging ENDPOINT = str(rh.TenantAPIs.REQUEST_ATTRIBUTES) @@ -15,13 +17,13 @@ def get_all_request_attributes(cluster, tenant): @returns list - list of Request Attributes from tenant """ logger.info("Getting all request attributes in tenant %s", tenant) - request_attributes = rh.make_api_call( + req_attrs = rh.make_api_call( cluster=cluster, tenant=tenant, endpoint=ENDPOINT ).json() - return request_attributes + return req_attrs def get_request_attribute_details(cluster, tenant, ra_id): @@ -167,9 +169,41 @@ def get_request_attribute_id(cluster, tenant, name): @returns str - ID of the request attribute if found. None otherwise. """ logger.info("Finding the ID for request attribute with name %s", name) - request_attributes = get_all_request_attributes(cluster, tenant) + req_attrs = get_all_request_attributes(cluster, tenant) - for req_attr in request_attributes: + for req_attr in req_attrs: if req_attr.get("name") == name: return req_attr.get("id") return None + + +def export_to_files(cluster, tenant, folder): + """Export all the request attributes in the tenant to files. + Each request attribute is written to a separate file in the folder provided. + The folder must exist already. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param folder (str) - path to folder where to write the export files + \n + @throws RuntimeException - when the specified folder is not found + """ + if not os.path.exists(folder): + try: + raise RuntimeError("Error: the given folder path does not exist") + except RuntimeError: + logger.exception("Error: folder path not found.", stack_info=True) + raise + + if "/" in folder and not folder.endswith("/"): + folder = f"{folder}/" + if "\\" in folder and not folder.endswith("\\"): + folder = f"{folder}\\" + + req_attrs = get_all_request_attributes(cluster, tenant) + + logger.info("Exporting request attributes. Writing files inside %s", folder) + for req_attr in req_attrs: + logger.debug("Exporting request attribute called %s", req_attr.get("name")) + with open(file=f"{folder}{req_attr.get('name')}.json", mode="w") as ra_file: + json.dump(req_attr, ra_file, indent=4) From e708e482be0e677e611219a20ae91029652202d2 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 18 Dec 2020 15:53:16 +0000 Subject: [PATCH 175/204] PAF-73: improvements resulted from testin --- dynatrace/tenant/request_attributes.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/dynatrace/tenant/request_attributes.py b/dynatrace/tenant/request_attributes.py index 71ac3b9..ac8cfc6 100644 --- a/dynatrace/tenant/request_attributes.py +++ b/dynatrace/tenant/request_attributes.py @@ -21,7 +21,7 @@ def get_all_request_attributes(cluster, tenant): cluster=cluster, tenant=tenant, endpoint=ENDPOINT - ).json() + ).json().get("values") return req_attrs @@ -143,7 +143,7 @@ def delete_request_attribute_by_name(cluster, tenant, ra_name): def create_or_update_request_attribute(cluster, tenant, ra_json): """Either creates a new request attribute from the provided JSON or updates it if it already exists in the tenant. Either way, the request attribute will be in the - tenant. + tenant. It will check if there is an ID present in the JSON for the update. \n @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n @@ -151,7 +151,7 @@ def create_or_update_request_attribute(cluster, tenant, ra_json): \n @returns Response - HTTP Response to the request """ - ra_id = get_request_attribute_id(cluster, tenant, ra_json.get("name")) + ra_id = ra_json.get("id") if ra_id: return update_request_attribute(cluster, tenant, ra_id, ra_json) @@ -159,20 +159,20 @@ def create_or_update_request_attribute(cluster, tenant, ra_json): return create_request_attribute(cluster, tenant, ra_json) -def get_request_attribute_id(cluster, tenant, name): +def get_request_attribute_id(cluster, tenant, ra_name): """Gets the ID for a request attribute referenced by name. \n @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n - @param name (str) - name of the Request Attribute + @param ra_name (str) - name of the Request Attribute \n @returns str - ID of the request attribute if found. None otherwise. """ - logger.info("Finding the ID for request attribute with name %s", name) + logger.info("Finding the ID for request attribute with name %s", ra_name) req_attrs = get_all_request_attributes(cluster, tenant) for req_attr in req_attrs: - if req_attr.get("name") == name: + if req_attr.get("name") == ra_name: return req_attr.get("id") return None @@ -186,7 +186,7 @@ def export_to_files(cluster, tenant, folder): @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n @param folder (str) - path to folder where to write the export files \n - @throws RuntimeException - when the specified folder is not found + @throws RuntimeError - when the specified folder is not found """ if not os.path.exists(folder): try: @@ -205,5 +205,6 @@ def export_to_files(cluster, tenant, folder): logger.info("Exporting request attributes. Writing files inside %s", folder) for req_attr in req_attrs: logger.debug("Exporting request attribute called %s", req_attr.get("name")) + ra_data = get_request_attribute_details(cluster, tenant, req_attr.get("id")) with open(file=f"{folder}{req_attr.get('name')}.json", mode="w") as ra_file: - json.dump(req_attr, ra_file, indent=4) + json.dump(ra_data, ra_file, indent=4) From 7e49050e8df937a792ac1452634fb4a3848e4d85 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 18 Dec 2020 15:53:46 +0000 Subject: [PATCH 176/204] PAF-73: testing cases for request_attributes --- .../request_attributes/definition.json | 21 ++ .../requests/request_attributes/updated.json | 22 ++ .../request_attributes/Mock_ReqAttr_1.json | 28 ++ .../responses/request_attributes/get_all.json | 16 + .../request_attributes/get_all_one.json | 8 + .../responses/request_attributes/get_one.json | 28 ++ tests/test_request_attributes.py | 299 ++++++++++++++++++ 7 files changed, 422 insertions(+) create mode 100644 tests/mockserver_payloads/requests/request_attributes/definition.json create mode 100644 tests/mockserver_payloads/requests/request_attributes/updated.json create mode 100644 tests/mockserver_payloads/responses/request_attributes/Mock_ReqAttr_1.json create mode 100644 tests/mockserver_payloads/responses/request_attributes/get_all.json create mode 100644 tests/mockserver_payloads/responses/request_attributes/get_all_one.json create mode 100644 tests/mockserver_payloads/responses/request_attributes/get_one.json create mode 100644 tests/test_request_attributes.py diff --git a/tests/mockserver_payloads/requests/request_attributes/definition.json b/tests/mockserver_payloads/requests/request_attributes/definition.json new file mode 100644 index 0000000..46ae206 --- /dev/null +++ b/tests/mockserver_payloads/requests/request_attributes/definition.json @@ -0,0 +1,21 @@ +{ + "name": "Mock_ReqAttr_4", + "enabled": true, + "dataType": "STRING", + "dataSources": [ + { + "enabled": true, + "source": "REQUEST_HEADER", + "valueProcessing": { + "splitAt": "", + "trim": false + }, + "parameterName": "x-mock-header", + "capturingAndStorageLocation": "CAPTURE_AND_STORE_ON_SERVER" + } + ], + "normalization": "ORIGINAL", + "aggregation": "FIRST", + "confidential": false, + "skipPersonalDataMasking": false +} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/request_attributes/updated.json b/tests/mockserver_payloads/requests/request_attributes/updated.json new file mode 100644 index 0000000..1fd0f56 --- /dev/null +++ b/tests/mockserver_payloads/requests/request_attributes/updated.json @@ -0,0 +1,22 @@ +{ + "id": "123abc456-a123-1234-4321-def123ghi45", + "name": "Mock_ReqAttr_1_update", + "enabled": true, + "dataType": "STRING", + "dataSources": [ + { + "enabled": true, + "source": "REQUEST_HEADER", + "valueProcessing": { + "splitAt": "", + "trim": false + }, + "parameterName": "x-mock-header", + "capturingAndStorageLocation": "CAPTURE_AND_STORE_ON_SERVER" + } + ], + "normalization": "ORIGINAL", + "aggregation": "FIRST", + "confidential": false, + "skipPersonalDataMasking": false +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/request_attributes/Mock_ReqAttr_1.json b/tests/mockserver_payloads/responses/request_attributes/Mock_ReqAttr_1.json new file mode 100644 index 0000000..c1b0880 --- /dev/null +++ b/tests/mockserver_payloads/responses/request_attributes/Mock_ReqAttr_1.json @@ -0,0 +1,28 @@ +{ + "metadata": { + "configurationVersions": [ + 1 + ], + "clusterVersion": "1.208.55.20201217-163132" + }, + "id": "123abc456-a123-1234-4321-def123ghi45", + "name": "Mock_ReqAttr_1", + "enabled": true, + "dataType": "STRING", + "dataSources": [ + { + "enabled": true, + "source": "REQUEST_HEADER", + "valueProcessing": { + "splitAt": "", + "trim": false + }, + "parameterName": "x-mock-header", + "capturingAndStorageLocation": "CAPTURE_AND_STORE_ON_SERVER" + } + ], + "normalization": "ORIGINAL", + "aggregation": "FIRST", + "confidential": false, + "skipPersonalDataMasking": false +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/request_attributes/get_all.json b/tests/mockserver_payloads/responses/request_attributes/get_all.json new file mode 100644 index 0000000..1a277c9 --- /dev/null +++ b/tests/mockserver_payloads/responses/request_attributes/get_all.json @@ -0,0 +1,16 @@ +{ + "values": [ + { + "id": "123abc456-a123-1234-4321-def123ghi45", + "name": "Mock_ReqAttr_1" + }, + { + "id": "456jkl789-b321-3210-0123-abc456def32", + "name": "Mock_ReqAttr_2" + }, + { + "id": "789mno012-c456-6789-9876-mno123abc12", + "name": "Mock_ReqAttr_3" + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/request_attributes/get_all_one.json b/tests/mockserver_payloads/responses/request_attributes/get_all_one.json new file mode 100644 index 0000000..05b032e --- /dev/null +++ b/tests/mockserver_payloads/responses/request_attributes/get_all_one.json @@ -0,0 +1,8 @@ +{ + "values": [ + { + "id": "123abc456-a123-1234-4321-def123ghi45", + "name": "Mock_ReqAttr_1" + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/request_attributes/get_one.json b/tests/mockserver_payloads/responses/request_attributes/get_one.json new file mode 100644 index 0000000..c1b0880 --- /dev/null +++ b/tests/mockserver_payloads/responses/request_attributes/get_one.json @@ -0,0 +1,28 @@ +{ + "metadata": { + "configurationVersions": [ + 1 + ], + "clusterVersion": "1.208.55.20201217-163132" + }, + "id": "123abc456-a123-1234-4321-def123ghi45", + "name": "Mock_ReqAttr_1", + "enabled": true, + "dataType": "STRING", + "dataSources": [ + { + "enabled": true, + "source": "REQUEST_HEADER", + "valueProcessing": { + "splitAt": "", + "trim": false + }, + "parameterName": "x-mock-header", + "capturingAndStorageLocation": "CAPTURE_AND_STORE_ON_SERVER" + } + ], + "normalization": "ORIGINAL", + "aggregation": "FIRST", + "confidential": false, + "skipPersonalDataMasking": false +} \ No newline at end of file diff --git a/tests/test_request_attributes.py b/tests/test_request_attributes.py new file mode 100644 index 0000000..13dc64a --- /dev/null +++ b/tests/test_request_attributes.py @@ -0,0 +1,299 @@ +"""Test suite for request_attributes""" + +import unittest +import os +import tests.tooling_for_test as testtools +from user_variables import FULL_SET +from dynatrace.framework.request_handler import TenantAPIs, HTTP +from dynatrace.tenant import request_attributes + +CLUSTER = FULL_SET["mockserver1"] +TENANT = "tenant1" +URL_PATH = str(TenantAPIs.REQUEST_ATTRIBUTES) +RA_ID = "123abc456-a123-1234-4321-def123ghi45" +RA_NAME = "Mock_ReqAttr_1" +REQUEST_DIR = "tests/mockserver_payloads/requests/request_attributes" +RESPONSE_DIR = "tests/mockserver_payloads/responses/request_attributes" + + +class TestRequest_Attributes(unittest.TestCase): + """Test cases for main functionality of the request_attributes module""" + + def test_create_or_update_request_attribute_U(self): + """Tests the create_or_update_request_attribute function. + Test the update portion of this function. + """ + request_file = f"{REQUEST_DIR}/updated.json" + response_file = f"{RESPONSE_DIR}/get_all.json" + ra_json = testtools.expected_payload(request_file) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file, + mock_id="Req1" + ) + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{RA_ID}", + request_type=str(HTTP.PUT), + response_code=204, + request_file=request_file, + mock_id="Req2" + ) + + result = request_attributes.create_or_update_request_attribute( + CLUSTER, TENANT, ra_json + ).status_code + expected_result = 204 + + self.assertEqual(result, expected_result) + + def test_create_or_update_request_attribute_C(self): + """Tests the create_or_update_request_attribute function. + Test the create portion of this function. + """ + request_file = f"{REQUEST_DIR}/definition.json" + response_file = f"{RESPONSE_DIR}/get_all.json" + ra_json = testtools.expected_payload(request_file) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file, + mock_id="Req1" + ) + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.POST), + response_code=201, + request_file=request_file, + mock_id="Req2" + ) + + result = request_attributes.create_or_update_request_attribute( + CLUSTER, TENANT, ra_json + ).status_code + expected_result = 201 + + self.assertEqual(result, expected_result) + + def test_create_request_attribute(self): + """Tests the create_request_attribute function""" + request_file = f"{REQUEST_DIR}/definition.json" + ra_json = testtools.expected_payload(request_file) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.POST), + response_code=201, + request_file=request_file + ) + + result = request_attributes.create_request_attribute( + CLUSTER, TENANT, ra_json + ).status_code + expected_result = 201 + + self.assertEqual(result, expected_result) + + def test_delete_request_attribute_by_id(self): + """Tests the delete_request_attribute_by_id function""" + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{RA_ID}", + request_type=str(HTTP.DELETE), + response_code=204 + ) + + result = request_attributes.delete_request_attribute_by_id( + CLUSTER, TENANT, RA_ID + ).status_code + expected_result = 204 + + self.assertEqual(result, expected_result) + + def test_delete_request_attribute_by_name(self): + """Tests the delete_request_attribute_by_name function""" + response_file = f"{RESPONSE_DIR}/get_all.json" + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file, + mock_id="Req1" + ) + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{RA_ID}", + request_type=str(HTTP.DELETE), + response_code=204, + mock_id="Req2" + ) + + result = request_attributes.delete_request_attribute_by_name( + CLUSTER, TENANT, RA_NAME + ).status_code + expected_result = 204 + + self.assertEqual(result, expected_result) + + def test_export_to_files(self): + """Tests the export_to_files function""" + response_file1 = f"{RESPONSE_DIR}/get_all_one.json" + response_file2 = f"{RESPONSE_DIR}/get_one.json" + folder = RESPONSE_DIR + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file1, + mock_id="Req1" + ) + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{RA_ID}", + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file2, + mock_id="Req2" + ) + + request_attributes.export_to_files(CLUSTER, TENANT, folder) + file = os.path.exists(f"{RESPONSE_DIR}/{RA_NAME}.json") + expected_file = True + file_data = testtools.expected_payload(f"{RESPONSE_DIR}/{RA_NAME}.json") + expected_file_data = testtools.expected_payload(response_file2) + + self.assertEqual(file, expected_file) + self.assertEqual(file_data, expected_file_data) + + def test_get_all_request_attributes(self): + """Tests the get_all_request_attributes function""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file + ) + + result = request_attributes.get_all_request_attributes(CLUSTER, TENANT) + expected_result = testtools.expected_payload(response_file).get("values") + + self.assertEqual(result, expected_result) + + def test_get_request_attribute_details(self): + """Tests the get_request_attribute_details function""" + response_file = f"{RESPONSE_DIR}/get_one.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{RA_ID}", + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file + ) + + result = request_attributes.get_request_attribute_details( + CLUSTER, TENANT, RA_ID + ) + expected_result = testtools.expected_payload(response_file) + + self.assertEqual(result, expected_result) + + def test_get_request_attribute_id(self): + """Tests the get_request_attribute_id function""" + response_file = f"{RESPONSE_DIR}/get_all.json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file + ) + + result = request_attributes.get_request_attribute_id(CLUSTER, TENANT, RA_NAME) + expected_result = RA_ID + + self.assertEqual(result, expected_result) + + def test_update_request_attribute(self): + """Tests the update_request_attribute function""" + request_file = f"{REQUEST_DIR}/updated.json" + ra_json = testtools.expected_payload(request_file) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{RA_ID}", + request_type=str(HTTP.PUT), + response_code=204, + request_file=request_file + ) + + result = request_attributes.update_request_attribute( + CLUSTER, TENANT, RA_ID, ra_json + ).status_code + expected_result = 204 + + self.assertEqual(result, expected_result) + + +class TestErrorHandling(unittest.TestCase): + def test_delete_request_attribute_by_name_RuntimeError(self): + """Tests error handling for function delete_request_attribute_by_name. + RuntimeError should be raised when request attribute ID is not found. + """ + response_file = f"{RESPONSE_DIR}/get_all.json" + RA_NAME = "invalid_name" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file + ) + + with self.assertRaises(RuntimeError): + request_attributes.delete_request_attribute_by_name(CLUSTER, TENANT, RA_NAME) + + def test_export_to_files_RuntimeError(self): + """Tests error handling for function export_to_files. + RuntimeError should be raised when export folder does not exist. + """ + FOLDER = "invalid_folder/path" + + with self.assertRaises(RuntimeError): + request_attributes.export_to_files(CLUSTER, TENANT, FOLDER) + + +if __name__ == "__main__": + unittest.main() From c5e34a0ad443d1f7f87543e299a0e1fe56d2109a Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Fri, 18 Dec 2020 16:14:49 +0000 Subject: [PATCH 177/204] PAF-73: de-smell -> better class name --- tests/test_request_attributes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_request_attributes.py b/tests/test_request_attributes.py index 13dc64a..f83c6a4 100644 --- a/tests/test_request_attributes.py +++ b/tests/test_request_attributes.py @@ -16,7 +16,7 @@ RESPONSE_DIR = "tests/mockserver_payloads/responses/request_attributes" -class TestRequest_Attributes(unittest.TestCase): +class TestRequestAttributes(unittest.TestCase): """Test cases for main functionality of the request_attributes module""" def test_create_or_update_request_attribute_U(self): From f45cb862b3db9a7c4ebf5282876559d4277149ab Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 19 Dec 2020 10:28:32 +0000 Subject: [PATCH 178/204] PAF-72: refurbish request_naming --- dynatrace/tenant/request_naming.py | 169 ++++++++++++++++++++++------- 1 file changed, 128 insertions(+), 41 deletions(-) diff --git a/dynatrace/tenant/request_naming.py b/dynatrace/tenant/request_naming.py index eaa1cbd..23b1ab1 100644 --- a/dynatrace/tenant/request_naming.py +++ b/dynatrace/tenant/request_naming.py @@ -1,46 +1,133 @@ -#!/bin/python3 -"""Global Service Request Naming Class""" +"""Service Request Naming Rule Operations via the Configuration API""" import os import json from dynatrace.framework import request_handler as rh -ENDPOINT = rh.TenantAPIs.REQUEST_NAMING - - -def pull_to_files(cluster, tenant, ignore_disabled=True): - """Pull Service Naming Rules to Files""" - all_rules_call = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=ENDPOINT) - all_rules_list = all_rules_call.json() - all_rules_list = all_rules_list['values'] - # print (json.dumps(all_rules_list, indent=2)) - - rules_file_list = [] - rule_num = 0 - for naming_rule in all_rules_list: - rule_call = rh.make_api_call(cluster=cluster, - tenant=tenant, - endpoint=f"{ENDPOINT}/{naming_rule['id']}") - if rule_call.status_code == 200: - rule_json = rule_call.json() - if rule_json['enabled'] and ignore_disabled: - rule_json.pop('metadata') - rule_json.pop('id') - rule_file_name = f"jsons/request_naming/{rule_num}.json" - with open(rule_file_name, 'w') as current_file: - json.dump(rule_json, current_file, indent=2) - rules_file_list.append(rule_file_name) - else: - print(rule_call.status_code) - rule_num = rule_num + 1 - return rules_file_list - - -def generate_file_list(): - """Generate File List from files in JSON directory""" - file_list = os.listdir("./jsons/request_naming/") - for file_name in file_list: - print(str.isdigit(file_name)) - # print(file_list.sort(key=lambda f: filter(str.isdigit, f))) +ENDPOINT = str(rh.TenantAPIs.REQUEST_NAMING) + + +def delete_naming_rule(cluster, tenant, rule_id): + """Deletes an already existing request naming rule, referenced by its ID. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param rule_id (str) - ID of the request naming rule to delete + \n + @returns Response - HTTP Response to the request + """ + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{rule_id}", + method=rh.HTTP.DELETE + ) + + return response + + +def update_naming_rule(cluster, tenant, rule_id, rule_json): + """Updates an already existing request naming rule, referenced by its ID. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param rule_id (str) - ID of the request naming rule to update\n + @param rule_json (dict) - new rule definition, to be sent as JSON payload + \n + @returns Response - HTTP Response to the request + """ + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{rule_id}", + method=rh.HTTP.PUT, + json=rule_json + ) + + return response + + +def create_naming_rule(cluster, tenant, rule_json): + """Creates a new request naming rule. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param rule_json (dict) - new rule definition, to be sent as JSON payload + \n + @returns Response - HTTP Response to the request + """ + response = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT, + method=rh.HTTP.POST, + json=rule_json + ) + + return response + + +def get_rule_details(cluster, tenant, rule_id): + """Gets the definition of an already existing request naming rule, referenced + by its ID. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param rule_id (str) - ID of the request naming rule to fetch + \n + @returns dict - the rule definition (details) + """ + details = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=f"{ENDPOINT}/{rule_id}", + method=rh.HTTP.GET + ).json() + + return details + + +def get_all_rules(cluster, tenant): + """Gets a list of all request naming rules in the tenant. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set + \n + @returns list - list of request naming rules + """ + rules = rh.make_api_call( + cluster=cluster, + tenant=tenant, + endpoint=ENDPOINT, + method=rh.HTTP.GET + ).json().get("values") + + return rules + + +def export_to_files(cluster, tenant, folder): + """Exports request naming rules from the tenant to .json files. + Each rule is written to its own file, in JSON format. The file is named after the + rule. + \n + @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n + @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n + @param folder (str) - path to folder where to create the files. + \n + @throws RuntimeError - when the folder path does not exist. + """ + if not os.path.exists(folder): + raise RuntimeError("Error: export folder path does not exist") + + if "/" in folder and not folder.endswith("/"): + folder += "/" + if "\\" in folder and not folder.endswith("\\"): + folder += "\\" + + rules = get_all_rules(cluster, tenant) + + for rule in rules: + rule_details = get_rule_details(cluster, tenant, rule.get("id")) + rule_name = rule.get("name") + with open(file=f"{folder}{rule_name}.json", mode="w") as rule_file: + json.dump(rule_details, rule_file, indent=4) From 30351ea46fd547ac14ee5f329c17a36356a81adb Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 19 Dec 2020 10:28:52 +0000 Subject: [PATCH 179/204] PAF-72: basic tests for request_naming --- .../requests/request_naming/definition.json | 34 ++++ .../requests/request_naming/updated.json | 34 ++++ .../responses/request_naming/get_all.json | 8 + .../responses/request_naming/get_one.json | 42 +++++ tests/test_request_naming.py | 156 ++++++++++++++++++ 5 files changed, 274 insertions(+) create mode 100644 tests/mockserver_payloads/requests/request_naming/definition.json create mode 100644 tests/mockserver_payloads/requests/request_naming/updated.json create mode 100644 tests/mockserver_payloads/responses/request_naming/get_all.json create mode 100644 tests/mockserver_payloads/responses/request_naming/get_one.json create mode 100644 tests/test_request_naming.py diff --git a/tests/mockserver_payloads/requests/request_naming/definition.json b/tests/mockserver_payloads/requests/request_naming/definition.json new file mode 100644 index 0000000..bd04263 --- /dev/null +++ b/tests/mockserver_payloads/requests/request_naming/definition.json @@ -0,0 +1,34 @@ +{ + "enabled": true, + "namingPattern": "{mock} request name", + "managementZones": [], + "conditions": [ + { + "attribute": "SERVICE_REQUEST_ATTRIBUTE", + "comparisonInfo": { + "type": "STRING_REQUEST_ATTRIBUTE", + "comparison": "EXISTS", + "value": null, + "negate": false, + "requestAttribute": "Mock Request Attribute", + "caseSensitive": false, + "matchOnChildCalls": false, + "source": null + } + } + ], + "placeholders": [ + { + "name": "mock", + "attribute": "SERVICE_REQUEST_ATTRIBUTE", + "kind": "REGEX_EXTRACTION", + "delimiterOrRegex": "(^.*?$)", + "endDelimiter": null, + "requestAttribute": "Mock Request Attribute", + "normalization": "ORIGINAL", + "useFromChildCalls": null, + "aggregation": null, + "source": null + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/requests/request_naming/updated.json b/tests/mockserver_payloads/requests/request_naming/updated.json new file mode 100644 index 0000000..b074a51 --- /dev/null +++ b/tests/mockserver_payloads/requests/request_naming/updated.json @@ -0,0 +1,34 @@ +{ + "enabled": true, + "namingPattern": "{mock} request name 123", + "managementZones": [], + "conditions": [ + { + "attribute": "SERVICE_REQUEST_ATTRIBUTE", + "comparisonInfo": { + "type": "STRING_REQUEST_ATTRIBUTE", + "comparison": "EXISTS", + "value": null, + "negate": false, + "requestAttribute": "Mock Request Attribute", + "caseSensitive": false, + "matchOnChildCalls": false, + "source": null + } + } + ], + "placeholders": [ + { + "name": "mock", + "attribute": "SERVICE_REQUEST_ATTRIBUTE", + "kind": "REGEX_EXTRACTION", + "delimiterOrRegex": "(^.*?$)", + "endDelimiter": null, + "requestAttribute": "Mock Request Attribute", + "normalization": "ORIGINAL", + "useFromChildCalls": null, + "aggregation": null, + "source": null + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/request_naming/get_all.json b/tests/mockserver_payloads/responses/request_naming/get_all.json new file mode 100644 index 0000000..de0c154 --- /dev/null +++ b/tests/mockserver_payloads/responses/request_naming/get_all.json @@ -0,0 +1,8 @@ +{ + "values": [ + { + "id": "abc1234def-1233-3321-ab123-abc123defghi", + "name": "{mock} request name" + } + ] +} \ No newline at end of file diff --git a/tests/mockserver_payloads/responses/request_naming/get_one.json b/tests/mockserver_payloads/responses/request_naming/get_one.json new file mode 100644 index 0000000..0b187c3 --- /dev/null +++ b/tests/mockserver_payloads/responses/request_naming/get_one.json @@ -0,0 +1,42 @@ + +{ + "metadata": { + "configurationVersions": [ + 0 + ], + "clusterVersion": "1.208.63.20201218-162412" + }, + "id": "abc1234def-1233-3321-ab123-abc123defghi", + "enabled": true, + "namingPattern": "{mock} request name", + "managementZones": [], + "conditions": [ + { + "attribute": "SERVICE_REQUEST_ATTRIBUTE", + "comparisonInfo": { + "type": "STRING_REQUEST_ATTRIBUTE", + "comparison": "EXISTS", + "value": null, + "negate": false, + "requestAttribute": "Mock Request Attribute", + "caseSensitive": false, + "matchOnChildCalls": false, + "source": null + } + } + ], + "placeholders": [ + { + "name": "mock", + "attribute": "SERVICE_REQUEST_ATTRIBUTE", + "kind": "REGEX_EXTRACTION", + "delimiterOrRegex": "(^.*?$)", + "endDelimiter": null, + "requestAttribute": "Mock Request Attribute", + "normalization": "ORIGINAL", + "useFromChildCalls": null, + "aggregation": null, + "source": null + } + ] +} \ No newline at end of file diff --git a/tests/test_request_naming.py b/tests/test_request_naming.py new file mode 100644 index 0000000..6c2ce71 --- /dev/null +++ b/tests/test_request_naming.py @@ -0,0 +1,156 @@ +"""Test suite for request_naming""" + +import unittest +import tests.tooling_for_test as testtools +from user_variables import FULL_SET +from dynatrace.framework.request_handler import TenantAPIs, HTTP +from dynatrace.tenant import request_naming + +CLUSTER = FULL_SET["mockserver1"] +TENANT = "tenant1" +URL_PATH = str(TenantAPIs.REQUEST_NAMING) +RULE_ID = "abc1234def-1233-3321-ab123-abc123defghi" +REQUEST_DIR = "tests/mockserver_payloads/requests/request_naming" +RESPONSE_DIR = "tests/mockserver_payloads/responses/request_naming" + + +class TestRequest_Naming(unittest.TestCase): + """Test cases for main functionality of the request_naming module""" + + def test_create_naming_rule(self): + """Tests the create_naming_rule function""" + request_file = f"{REQUEST_DIR}/definition.json" + rule_json = testtools.expected_payload(request_file) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.POST), + response_code=201, + request_file=request_file + ) + + result = request_naming.create_naming_rule(CLUSTER, TENANT, rule_json).status_code + expected_result = 201 + + self.assertEqual(result, expected_result) + + def test_delete_naming_rule(self): + """Tests the delete_naming_rule function""" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{RULE_ID}", + request_type=str(HTTP.DELETE), + response_code=204 + ) + + result = request_naming.delete_naming_rule(CLUSTER, TENANT, RULE_ID).status_code + expected_result = 204 + + self.assertEqual(result, expected_result) + + def test_export_to_files(self): + """Tests the export_to_files function""" + response_file1 = f"{RESPONSE_DIR}/get_all.json" + response_file2 = f"{RESPONSE_DIR}/get_one.json" + rules_list = testtools.expected_payload(response_file1).get("values") + rule_json = testtools.expected_payload(response_file2) + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file1, + mock_id="Req1" + ) + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{RULE_ID}", + request_type=str(HTTP.GET), + response_code=200, + response_file=response_file2, + mock_id="Req2" + ) + + request_naming.export_to_files(CLUSTER, TENANT, RESPONSE_DIR) + file = f"{rules_list[0].get('name')}.json" + expected_file = + + self.assertEqual(result, expected_result) + + def test_get_all_rules(self): + """Tests the get_all_rules function""" + request_file = f"{REQUEST_DIR}/[req_file].json" + response_file = f"{RESPONSE_DIR}/[res_file].json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=URL_PATH, + request_type=str(HTTP.GET), + response_code=200, + request_file=request_file, + response_file=response_file + ) + + result = request_naming.get_all_rules(CLUSTER, TENANT) + expected_result = None + + self.assertEqual(result, expected_result) + + def test_get_rule_details(self): + """Tests the get_rule_details function""" + request_file = f"{REQUEST_DIR}/[req_file].json" + response_file = f"{RESPONSE_DIR}/[res_file].json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{rule_id}", + request_type=str(HTTP.GET), + response_code=200, + request_file=request_file, + response_file=response_file + ) + + result = request_naming.get_rule_details(CLUSTER, TENANT, RULE_ID) + expected_result = None + + self.assertEqual(result, expected_result) + + def test_update_naming_rule(self): + """Tests the update_naming_rule function""" + request_file = f"{REQUEST_DIR}/[req_file].json" + + testtools.create_mockserver_expectation( + cluster=CLUSTER, + tenant=TENANT, + url_path=f"{URL_PATH}/{rule_id}", + request_type=str(HTTP.PUT), + response_code=204, + request_file=request_file + ) + + result = request_naming.update_naming_rule(CLUSTER, TENANT, RULE_ID, RULE_JSON) + expected_result = None + + self.assertEqual(result, expected_result) + + +class TestErrorHandling(unittest.TestCase): + def test_export_to_files_RuntimeError(self): + """Tests error handling for function export_to_files. + RuntimeError should be raised when when the folder path does not exist. + """ + with self.assertRaises(RuntimeError): + request_naming.export_to_files(CLUSTER, TENANT, FOLDER) + + +if __name__ == "__main__": + unittest.main() From 83f1699c4f35ee66024b2851c850f5dad836c128 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 19 Dec 2020 10:39:50 +0000 Subject: [PATCH 180/204] PAF-72: finished validating test suite --- .../request_naming/{mock} request name.json | 41 ++++++++++++++++++ tests/test_request_naming.py | 43 +++++++++++-------- 2 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 tests/mockserver_payloads/responses/request_naming/{mock} request name.json diff --git a/tests/mockserver_payloads/responses/request_naming/{mock} request name.json b/tests/mockserver_payloads/responses/request_naming/{mock} request name.json new file mode 100644 index 0000000..0d7e779 --- /dev/null +++ b/tests/mockserver_payloads/responses/request_naming/{mock} request name.json @@ -0,0 +1,41 @@ +{ + "metadata": { + "configurationVersions": [ + 0 + ], + "clusterVersion": "1.208.63.20201218-162412" + }, + "id": "abc1234def-1233-3321-ab123-abc123defghi", + "enabled": true, + "namingPattern": "{mock} request name", + "managementZones": [], + "conditions": [ + { + "attribute": "SERVICE_REQUEST_ATTRIBUTE", + "comparisonInfo": { + "type": "STRING_REQUEST_ATTRIBUTE", + "comparison": "EXISTS", + "value": null, + "negate": false, + "requestAttribute": "Mock Request Attribute", + "caseSensitive": false, + "matchOnChildCalls": false, + "source": null + } + } + ], + "placeholders": [ + { + "name": "mock", + "attribute": "SERVICE_REQUEST_ATTRIBUTE", + "kind": "REGEX_EXTRACTION", + "delimiterOrRegex": "(^.*?$)", + "endDelimiter": null, + "requestAttribute": "Mock Request Attribute", + "normalization": "ORIGINAL", + "useFromChildCalls": null, + "aggregation": null, + "source": null + } + ] +} \ No newline at end of file diff --git a/tests/test_request_naming.py b/tests/test_request_naming.py index 6c2ce71..96cb6ff 100644 --- a/tests/test_request_naming.py +++ b/tests/test_request_naming.py @@ -1,6 +1,7 @@ """Test suite for request_naming""" import unittest +import os import tests.tooling_for_test as testtools from user_variables import FULL_SET from dynatrace.framework.request_handler import TenantAPIs, HTTP @@ -57,7 +58,6 @@ def test_export_to_files(self): response_file1 = f"{RESPONSE_DIR}/get_all.json" response_file2 = f"{RESPONSE_DIR}/get_one.json" rules_list = testtools.expected_payload(response_file1).get("values") - rule_json = testtools.expected_payload(response_file2) testtools.create_mockserver_expectation( cluster=CLUSTER, @@ -79,15 +79,19 @@ def test_export_to_files(self): ) request_naming.export_to_files(CLUSTER, TENANT, RESPONSE_DIR) - file = f"{rules_list[0].get('name')}.json" - expected_file = + file = os.path.exists(f"{RESPONSE_DIR}/{rules_list[0].get('name')}.json") + expected_file = True + file_data = testtools.expected_payload( + f"{RESPONSE_DIR}/{rules_list[0].get('name')}.json" + ) + expected_file_data = testtools.expected_payload(response_file2) - self.assertEqual(result, expected_result) + self.assertEqual(file, expected_file) + self.assertEqual(file_data, expected_file_data) def test_get_all_rules(self): """Tests the get_all_rules function""" - request_file = f"{REQUEST_DIR}/[req_file].json" - response_file = f"{RESPONSE_DIR}/[res_file].json" + response_file = f"{RESPONSE_DIR}/get_all.json" testtools.create_mockserver_expectation( cluster=CLUSTER, @@ -95,50 +99,50 @@ def test_get_all_rules(self): url_path=URL_PATH, request_type=str(HTTP.GET), response_code=200, - request_file=request_file, response_file=response_file ) result = request_naming.get_all_rules(CLUSTER, TENANT) - expected_result = None + expected_result = testtools.expected_payload(response_file).get("values") self.assertEqual(result, expected_result) def test_get_rule_details(self): """Tests the get_rule_details function""" - request_file = f"{REQUEST_DIR}/[req_file].json" - response_file = f"{RESPONSE_DIR}/[res_file].json" + response_file = f"{RESPONSE_DIR}/get_one.json" testtools.create_mockserver_expectation( cluster=CLUSTER, tenant=TENANT, - url_path=f"{URL_PATH}/{rule_id}", + url_path=f"{URL_PATH}/{RULE_ID}", request_type=str(HTTP.GET), response_code=200, - request_file=request_file, response_file=response_file ) result = request_naming.get_rule_details(CLUSTER, TENANT, RULE_ID) - expected_result = None + expected_result = testtools.expected_payload(response_file) self.assertEqual(result, expected_result) def test_update_naming_rule(self): """Tests the update_naming_rule function""" - request_file = f"{REQUEST_DIR}/[req_file].json" + request_file = f"{REQUEST_DIR}/updated.json" + rule_json = testtools.expected_payload(request_file) testtools.create_mockserver_expectation( cluster=CLUSTER, tenant=TENANT, - url_path=f"{URL_PATH}/{rule_id}", + url_path=f"{URL_PATH}/{RULE_ID}", request_type=str(HTTP.PUT), response_code=204, request_file=request_file ) - result = request_naming.update_naming_rule(CLUSTER, TENANT, RULE_ID, RULE_JSON) - expected_result = None + result = request_naming.update_naming_rule( + CLUSTER, TENANT, RULE_ID, rule_json + ).status_code + expected_result = 204 self.assertEqual(result, expected_result) @@ -146,10 +150,11 @@ def test_update_naming_rule(self): class TestErrorHandling(unittest.TestCase): def test_export_to_files_RuntimeError(self): """Tests error handling for function export_to_files. - RuntimeError should be raised when when the folder path does not exist. + RuntimeError should be raised when the folder path does not exist. """ + folder = "nonexistent/folder/path" with self.assertRaises(RuntimeError): - request_naming.export_to_files(CLUSTER, TENANT, FOLDER) + request_naming.export_to_files(CLUSTER, TENANT, folder) if __name__ == "__main__": From 747cd6fb01b1c7fe15db564a75b19ae6aaad7ac2 Mon Sep 17 00:00:00 2001 From: Radu Stefan Date: Sat, 19 Dec 2020 11:02:29 +0000 Subject: [PATCH 181/204] PAF-72: de-smell -> renamed Test Case class --- tests/test_request_naming.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_request_naming.py b/tests/test_request_naming.py index 96cb6ff..1aa7385 100644 --- a/tests/test_request_naming.py +++ b/tests/test_request_naming.py @@ -15,7 +15,7 @@ RESPONSE_DIR = "tests/mockserver_payloads/responses/request_naming" -class TestRequest_Naming(unittest.TestCase): +class TestRequestNaming(unittest.TestCase): """Test cases for main functionality of the request_naming module""" def test_create_naming_rule(self): From d9c68487d6881d9691098c65ceced3b37c86a8c7 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 8 Feb 2021 23:44:39 -0500 Subject: [PATCH 182/204] PAF-70 #Adding Logic to handle managed requests --- .circleci/config.yml | 2 +- .circleci/mockserver.py | 4 ++-- tests/tooling_for_test.py | 11 +++++++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4b229aa..c4125ff 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ jobs: environment: PIPENV_VENV_IN_PROJECT: true - image: mockserver/mockserver - name: mockserver.mockserver + name: mockserver steps: - checkout # check out source code to working directory - run: diff --git a/.circleci/mockserver.py b/.circleci/mockserver.py index 96fd562..e53b848 100644 --- a/.circleci/mockserver.py +++ b/.circleci/mockserver.py @@ -3,13 +3,13 @@ "mockserver1": { "url": "mockserver:1080", "tenant": { - "tenant1": "mockserver", + "tenant1": "mockenv", }, "api_token": { "tenant1": "sample_api_token", }, "verify_ssl": False, - "is_managed": False, + "is_managed": True, "cluster_token": "Required for Cluster Operations in Managed" } } diff --git a/tests/tooling_for_test.py b/tests/tooling_for_test.py index 8fa3092..9ece8a1 100644 --- a/tests/tooling_for_test.py +++ b/tests/tooling_for_test.py @@ -24,12 +24,20 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa @throws ValueError - when the response code is not positive """ requests.packages.urllib3.disable_warnings() # pylint: disable=no-member + + if cluster.get("is_managed"): + expected_path = f"/e/{cluster.get('tenant').get(tenant)}{url_path}" + expectation_url = f"http://{cluster['url']}/mockserver/expectation" + else: + expected_path = url_path + expectation_url = f"{generate_tenant_url(cluster, tenant)}/mockserver/expectation" + expectation = { "httpRequest": { "headers": { "Authorization": [f"Api-Token {cluster.get('api_token').get(tenant)}"] }, - "path": url_path, + "path": expected_path, "method": request_type }, "httpResponse": { @@ -93,7 +101,6 @@ def create_mockserver_expectation(cluster, tenant, url_path, request_type, **kwa logging.debug(expectation) - expectation_url = f"{generate_tenant_url(cluster, tenant)}/mockserver/expectation" test_req = requests.request( "PUT", expectation_url, From 2b399dd5c0b2b92b7dec1fd768f3d17638084482 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 20 Feb 2021 22:39:44 -0500 Subject: [PATCH 183/204] Supports YAML configuration file --- Pipfile | 1 + Pipfile.lock | 183 ++++++++++++++++++++------------ dynatrace/framework/settings.py | 62 ++++++++++- 3 files changed, 175 insertions(+), 71 deletions(-) diff --git a/Pipfile b/Pipfile index 897d6db..5ce3d44 100644 --- a/Pipfile +++ b/Pipfile @@ -11,6 +11,7 @@ flake8 = "*" [packages] requests = "*" +pyyaml = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index ddbc73a..5241027 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "821caf8bcd1fbece0d9ec4853bfb6fd206066401b8eceb209b95e7f77629dc2b" + "sha256": "1f6dcbeb815f9efa753d891f95091be3a22aafd06ea237ff11fd5094c8fef0c4" }, "pipfile-spec": 6, "requires": { @@ -18,41 +18,66 @@ "default": { "certifi": { "hashes": [ - "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", - "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2020.6.20" + "version": "==2020.12.5" }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "version": "==4.0.0" }, "idna": { "hashes": [ "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.10" }, + "pyyaml": { + "hashes": [ + "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf", + "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696", + "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393", + "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77", + "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922", + "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5", + "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8", + "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10", + "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc", + "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", + "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", + "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", + "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", + "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", + "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc" + ], + "index": "pypi", + "version": "==5.4.1" + }, "requests": { "hashes": [ - "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", - "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], "index": "pypi", - "version": "==2.24.0" + "version": "==2.25.1" }, "urllib3": { "hashes": [ - "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", - "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80", + "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.25.10" + "version": "==1.26.3" } }, "develop": { @@ -61,71 +86,93 @@ "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703", "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386" ], - "markers": "python_version >= '3.5'", "version": "==2.4.2" }, "autopep8": { "hashes": [ - "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094" + "sha256:9e136c472c475f4ee4978b51a88a494bfcd4e3ed17950a44a988d9e434837bea", + "sha256:cae4bc0fb616408191af41d062d7ec7ef8679c7f27b068875ca3a9e2878d5443" ], "index": "pypi", - "version": "==1.5.4" + "version": "==1.5.5" + }, + "colorama": { + "hashes": [ + "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b", + "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2" + ], + "markers": "sys_platform == 'win32'", + "version": "==0.4.4" }, "coverage": { "hashes": [ - "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", - "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", - "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", - "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", - "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", - "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", - "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", - "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", - "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", - "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", - "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", - "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", - "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", - "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", - "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", - "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", - "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", - "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", - "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", - "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", - "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", - "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", - "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", - "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", - "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", - "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", - "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", - "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", - "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", - "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", - "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", - "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", - "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", - "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" + "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7", + "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5", + "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f", + "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde", + "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f", + "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f", + "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c", + "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66", + "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90", + "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337", + "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d", + "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4", + "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409", + "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37", + "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1", + "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247", + "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39", + "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c", + "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994", + "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c", + "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb", + "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc", + "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f", + "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca", + "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135", + "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3", + "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339", + "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9", + "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9", + "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af", + "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370", + "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19", + "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3", + "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44", + "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3", + "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a", + "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c", + "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b", + "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9", + "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8", + "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22", + "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f", + "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345", + "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880", + "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0", + "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b", + "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec", + "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3", + "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786" ], "index": "pypi", - "version": "==5.3" + "version": "==5.4" }, "flake8": { "hashes": [ - "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c", - "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208" + "sha256:749dbbd6bfd0cf1318af27bf97a14e28e5ff548ef8e5b1566ccfb25a11e7c839", + "sha256:aadae8761ec651813c24be05c6f7b4680857ef6afaae4651a4eccaef97ce6c3b" ], "index": "pypi", - "version": "==3.8.3" + "version": "==3.8.4" }, "isort": { "hashes": [ - "sha256:171c5f365791073426b5ed3a156c2081a47f88c329161fd28228ff2da4c97ddb", - "sha256:ba91218eee31f1e300ecc079ef0c524cea3fc41bfbb979cbdf5fd3a889e3cfed" + "sha256:c729845434366216d320e936b8ad6f9d681aab72dc7cbc2d51bedc3582f3ad1e", + "sha256:fff4f0c04e1825522ce6949973e83110a6e907750cd92d128b0d14aaaadbffdc" ], - "markers": "python_version >= '3.6' and python_version < '4.0'", - "version": "==5.5.2" + "version": "==5.7.0" }, "lazy-object-proxy": { "hashes": [ @@ -151,7 +198,6 @@ "sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4", "sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.4.3" }, "mccabe": { @@ -166,7 +212,6 @@ "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367", "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.6.0" }, "pyflakes": { @@ -174,31 +219,29 @@ "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92", "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.2.0" }, "pylint": { "hashes": [ - "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210", - "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f" + "sha256:718b74786ea7ed07aa0c58bf572154d4679f960d26e9641cc1de204a30b87fc9", + "sha256:e71c2e9614a4f06e36498f310027942b0f4f2fde20aebb01655b31edc63b9eaf" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.6.2" }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.15.0" }, "toml": { "hashes": [ - "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f", - "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88" + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" ], - "version": "==0.10.1" + "version": "==0.10.2" }, "wrapt": { "hashes": [ diff --git a/dynatrace/framework/settings.py b/dynatrace/framework/settings.py index fd3b1b0..46f2dc6 100644 --- a/dynatrace/framework/settings.py +++ b/dynatrace/framework/settings.py @@ -1,11 +1,11 @@ """Module for retreiving settings safely""" +import yaml try: import user_variables # pylint: disable=import-error FILE_IMPORTED = True except ImportError: FILE_IMPORTED = False - DefaultSettings = { "LOG_LEVEL": "ERROR", "LOG_DIR": "logs/", @@ -38,6 +38,8 @@ 'DEFAULT_TIMEZONE': "UTC", } +__IMPORTED_SETTINGS__ = None + def get_setting(attribute): """Fetch setting from user-defined files or else default values @@ -51,9 +53,67 @@ def get_setting(attribute): Returns: [type]: attribute in it's correct variable type if found """ + global __IMPORTED_SETTINGS__ + if attribute in __IMPORTED_SETTINGS__: + return __IMPORTED_SETTINGS__[attribute] + # print (__IMPORTED_SETTINGS__) if FILE_IMPORTED and hasattr(user_variables, attribute): return getattr(user_variables, attribute) if attribute in DefaultSettings: return DefaultSettings[attribute] raise AttributeError( f"{attribute} is not a valid user variable attribute!") + +def create_cluster(url, tenant_ids, tenant_tokens, **kwargs): + """Allow user to dynamically create cluster + \n + @param tenant_ids (dict) - Dynatrace tenant name or dictionary of tenant ids\n + @param tenant_tokens (dict) - Dynatrace tenant tokens\n + @param cluster_token (str) - Dynatrace cluster tokens\n + \n + @kwargs cluster_token - provide cluster_token (Managed only)\n + @kwargs verify_ssl - Verify SSL Cert. Either Bool or path to cert\n + @kwargs is_managed - Manual flag if cluster is a managed instance\n\n + @return - number of entities + """ + + verify_ssl = True if 'verify_ssl' not in kwargs else kwargs['verify_ssl'] + is_managed = False if 'is_managed' not in kwargs else kwargs['is_managed'] + + cluster = { + 'url': url, + 'tenant': {}, + 'api_token': {}, + 'verify_ssl': verify_ssl, + 'is_managed': is_managed, + } + + if 'cluster_token' in kwargs: + cluster['cluster_token'] = kwargs['cluster_token'] + + if isinstance(tenant_ids, dict) and isinstance(tenant_tokens, dict): + for tenant in tenant_ids: + cluster['tenant'][tenant] = tenant_ids [tenant] + cluster['api_token'][tenant] = tenant_tokens [tenant] + else: + raise ValueError("tenant and tenant token must both be dict") + return cluster + +def add_tenant_to_cluster(cluster, tenant_id, tenant_token, tenant_name): + """Add tenant to predefined cluster""" + if isinstance(tenant_id, str) and isinstance(tenant_token, str): + cluster ['tenant'] [tenant_name] = tenant_id + cluster ['api_token'] [tenant_name] = tenant_token + +def load_settings_from_file(settings_file): + """Assign setting value)s as defined by the cluster""" + global __IMPORTED_SETTINGS__ + + if str.endswith(settings_file, ".yaml") or str.endswith(settings_file, ".yml"): + with open(settings_file) as file: + imported_settings = yaml.load(file, Loader=yaml.FullLoader) + if __IMPORTED_SETTINGS__ is None: + __IMPORTED_SETTINGS__ = imported_settings + else: + for setting, value in imported_settings.items(): + __IMPORTED_SETTINGS__[setting] = value From fdbb4d3f5eb49caa337ecacf80b9d169188ccd6a Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 20 Feb 2021 23:14:37 -0500 Subject: [PATCH 184/204] Adding JSON import capability --- dynatrace/framework/settings.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/dynatrace/framework/settings.py b/dynatrace/framework/settings.py index 46f2dc6..2749368 100644 --- a/dynatrace/framework/settings.py +++ b/dynatrace/framework/settings.py @@ -1,5 +1,6 @@ """Module for retreiving settings safely""" import yaml +import json try: import user_variables # pylint: disable=import-error FILE_IMPORTED = True @@ -112,8 +113,13 @@ def load_settings_from_file(settings_file): if str.endswith(settings_file, ".yaml") or str.endswith(settings_file, ".yml"): with open(settings_file) as file: imported_settings = yaml.load(file, Loader=yaml.FullLoader) - if __IMPORTED_SETTINGS__ is None: - __IMPORTED_SETTINGS__ = imported_settings - else: - for setting, value in imported_settings.items(): - __IMPORTED_SETTINGS__[setting] = value + + if str.endswith(settings_file, ".json"): + with open(settings_file) as file: + imported_settings = json.load(file) + + if __IMPORTED_SETTINGS__ is None: + __IMPORTED_SETTINGS__ = imported_settings + else: + for setting, value in imported_settings.items(): + __IMPORTED_SETTINGS__[setting] = value From 9e3754ad89908fd6fc6363fb8a4a78624b0b4776 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sat, 20 Feb 2021 23:18:08 -0500 Subject: [PATCH 185/204] Defaulting Global var to empty set --- dynatrace/framework/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/framework/settings.py b/dynatrace/framework/settings.py index 2749368..222a956 100644 --- a/dynatrace/framework/settings.py +++ b/dynatrace/framework/settings.py @@ -39,7 +39,7 @@ 'DEFAULT_TIMEZONE': "UTC", } -__IMPORTED_SETTINGS__ = None +__IMPORTED_SETTINGS__ = {} def get_setting(attribute): From e375f582715a356d14b3c0fe82c76c306b6d133f Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 21 Feb 2021 12:02:21 -0500 Subject: [PATCH 186/204] PAF-44 #user_variables to be absorbed by new var --- dynatrace/framework/settings.py | 56 +++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/dynatrace/framework/settings.py b/dynatrace/framework/settings.py index 222a956..1e95ec5 100644 --- a/dynatrace/framework/settings.py +++ b/dynatrace/framework/settings.py @@ -1,11 +1,16 @@ """Module for retreiving settings safely""" -import yaml import json +import yaml + +__IMPORTED_SETTINGS__ = {} + try: import user_variables # pylint: disable=import-error - FILE_IMPORTED = True + for attr in dir(user_variables): + if not str.startswith(attr, "__"): + __IMPORTED_SETTINGS__[attr] = getattr(user_variables, attr) except ImportError: - FILE_IMPORTED = False + pass DefaultSettings = { "LOG_LEVEL": "ERROR", @@ -39,7 +44,7 @@ 'DEFAULT_TIMEZONE': "UTC", } -__IMPORTED_SETTINGS__ = {} + def get_setting(attribute): @@ -54,24 +59,35 @@ def get_setting(attribute): Returns: [type]: attribute in it's correct variable type if found """ - global __IMPORTED_SETTINGS__ + global __IMPORTED_SETTINGS__ # pylint: disable=global-statement if attribute in __IMPORTED_SETTINGS__: return __IMPORTED_SETTINGS__[attribute] - # print (__IMPORTED_SETTINGS__) - if FILE_IMPORTED and hasattr(user_variables, attribute): - return getattr(user_variables, attribute) if attribute in DefaultSettings: return DefaultSettings[attribute] raise AttributeError( f"{attribute} is not a valid user variable attribute!") -def create_cluster(url, tenant_ids, tenant_tokens, **kwargs): +def get_cluster_dict(cluster): + """Get Cluster Dict\n + @param cluster_name (dict, str) - Name of the cluster to return\n + @return - Cluster dictionary + """ + if isinstance(cluster, dict): + return cluster + + if 'FULL_SET' in __IMPORTED_SETTINGS__ and cluster in __IMPORTED_SETTINGS__['FULL_SET']: + return __IMPORTED_SETTINGS__['FULL_SET'][cluster] + + raise ValueError ("Cluster not found") + +def create_cluster(cluster_name, url, **kwargs): """Allow user to dynamically create cluster \n - @param tenant_ids (dict) - Dynatrace tenant name or dictionary of tenant ids\n - @param tenant_tokens (dict) - Dynatrace tenant tokens\n - @param cluster_token (str) - Dynatrace cluster tokens\n + @param cluster_name (str) - Name of cluster to be added to the Cluster Set \n + @param url (str) - URL for cluster \n \n + @kwargs tenant_ids (dict) - Dynatrace tenant name or dictionary of tenant ids\n + @kwargs tenant_tokens (dict) - Dynatrace tenant tokens\n @kwargs cluster_token - provide cluster_token (Managed only)\n @kwargs verify_ssl - Verify SSL Cert. Either Bool or path to cert\n @kwargs is_managed - Manual flag if cluster is a managed instance\n\n @@ -79,7 +95,9 @@ def create_cluster(url, tenant_ids, tenant_tokens, **kwargs): """ verify_ssl = True if 'verify_ssl' not in kwargs else kwargs['verify_ssl'] - is_managed = False if 'is_managed' not in kwargs else kwargs['is_managed'] + is_managed = True if 'is_managed' not in kwargs else kwargs['is_managed'] + tenant_ids = None if 'tenant_ids' not in kwargs else kwargs['tenant_ids'] + tenant_tokens = None if 'tenant_tokens' not in kwargs else kwargs['tenant_tokens'] cluster = { 'url': url, @@ -96,9 +114,15 @@ def create_cluster(url, tenant_ids, tenant_tokens, **kwargs): for tenant in tenant_ids: cluster['tenant'][tenant] = tenant_ids [tenant] cluster['api_token'][tenant] = tenant_tokens [tenant] + elif tenant_ids is None and tenant_tokens is None: + pass else: raise ValueError("tenant and tenant token must both be dict") - return cluster + + if 'FULL_SET' not in __IMPORTED_SETTINGS__: + __IMPORTED_SETTINGS__['FULL_SET'] = {} + __IMPORTED_SETTINGS__['FULL_SET'][cluster_name] = cluster + return __IMPORTED_SETTINGS__['FULL_SET'][cluster_name] def add_tenant_to_cluster(cluster, tenant_id, tenant_token, tenant_name): """Add tenant to predefined cluster""" @@ -108,7 +132,7 @@ def add_tenant_to_cluster(cluster, tenant_id, tenant_token, tenant_name): def load_settings_from_file(settings_file): """Assign setting value)s as defined by the cluster""" - global __IMPORTED_SETTINGS__ + global __IMPORTED_SETTINGS__ # pylint: disable=global-statement if str.endswith(settings_file, ".yaml") or str.endswith(settings_file, ".yml"): with open(settings_file) as file: @@ -118,7 +142,7 @@ def load_settings_from_file(settings_file): with open(settings_file) as file: imported_settings = json.load(file) - if __IMPORTED_SETTINGS__ is None: + if __IMPORTED_SETTINGS__ == {}: __IMPORTED_SETTINGS__ = imported_settings else: for setting, value in imported_settings.items(): From 5d7a949057afa3af8b9444c17e78f2a9951a5b31 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 21 Feb 2021 18:37:04 -0500 Subject: [PATCH 187/204] PAF-44 #Adding more tests --- .gitignore | 3 +- dynatrace/framework/settings.py | 21 +-- tests/special_test_settings_no_variables.py | 15 ++- tests/test_settings.py | 140 +++++++++++++++++++- 4 files changed, 168 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 1c60234..454aaa9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,8 +14,9 @@ templates/* ***.tap** .converge -user_variables.py +user_variables.* sandbox_script.py +htmlcov/* !variable_sets/template.py !scripts/template.py diff --git a/dynatrace/framework/settings.py b/dynatrace/framework/settings.py index 1e95ec5..1f672bf 100644 --- a/dynatrace/framework/settings.py +++ b/dynatrace/framework/settings.py @@ -117,7 +117,7 @@ def create_cluster(cluster_name, url, **kwargs): elif tenant_ids is None and tenant_tokens is None: pass else: - raise ValueError("tenant and tenant token must both be dict") + raise ValueError("Tenant and tenant token must both be dict") if 'FULL_SET' not in __IMPORTED_SETTINGS__: __IMPORTED_SETTINGS__['FULL_SET'] = {} @@ -126,9 +126,17 @@ def create_cluster(cluster_name, url, **kwargs): def add_tenant_to_cluster(cluster, tenant_id, tenant_token, tenant_name): """Add tenant to predefined cluster""" + if isinstance (cluster, dict): + raise NotImplementedError( + "Cluster dicts are not supported yet. Please use str for the cluster's key" + ) if isinstance(tenant_id, str) and isinstance(tenant_token, str): - cluster ['tenant'] [tenant_name] = tenant_id - cluster ['api_token'] [tenant_name] = tenant_token + if cluster in __IMPORTED_SETTINGS__['FULL_SET']: + __IMPORTED_SETTINGS__['FULL_SET'][cluster]['tenant'][tenant_name] = tenant_id + __IMPORTED_SETTINGS__['FULL_SET'][cluster]['api_token'][tenant_name] = tenant_token + else: + raise KeyError("Cluster not found") + return __IMPORTED_SETTINGS__['FULL_SET'][cluster] def load_settings_from_file(settings_file): """Assign setting value)s as defined by the cluster""" @@ -142,8 +150,5 @@ def load_settings_from_file(settings_file): with open(settings_file) as file: imported_settings = json.load(file) - if __IMPORTED_SETTINGS__ == {}: - __IMPORTED_SETTINGS__ = imported_settings - else: - for setting, value in imported_settings.items(): - __IMPORTED_SETTINGS__[setting] = value + for setting, value in imported_settings.items(): + __IMPORTED_SETTINGS__[setting] = value diff --git a/tests/special_test_settings_no_variables.py b/tests/special_test_settings_no_variables.py index 87b2e52..6e58d70 100644 --- a/tests/special_test_settings_no_variables.py +++ b/tests/special_test_settings_no_variables.py @@ -2,7 +2,6 @@ import unittest from os import rename - class TestSettingsWithoutVarFile(unittest.TestCase): """Special test cases when user_variables is absent""" def test_settings_without_var_file(self): @@ -12,3 +11,17 @@ def test_settings_without_var_file(self): timezone = settings.get_setting("DEFAULT_TIMEZONE") self.assertEqual(timezone, "UTC") rename("user_variables.py.tmp", "user_variables.py") + def test_cluster_creation_from_nothing(self): + """Create Cluster without a preexisting user_variables""" + rename("user_variables.py", "user_variables.py.tmp") + from dynatrace.framework import settings # pylint: disable=import-outside-toplevel + new_cluster = settings.create_cluster("cluster", "test.site") + expected_cluster = { + "url": "test.site", + 'tenant': {}, + 'api_token': {}, + 'verify_ssl': True, + 'is_managed': True, + } + self.assertEqual(new_cluster, expected_cluster) + rename("user_variables.py.tmp", "user_variables.py") diff --git a/tests/test_settings.py b/tests/test_settings.py index 87c854f..33f8679 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -1,8 +1,11 @@ """Test Cases for dynatrace.settings""" import unittest +import yaml from dynatrace.framework import settings - +SETTINGS_FILE = "user_variables.yaml" +with open(SETTINGS_FILE) as file: + IMPORTED_SETTINGS = yaml.load(file, Loader=yaml.FullLoader) class TestSettings(unittest.TestCase): """Standard Testing Class""" def test_get_setting_from_user_variable(self): @@ -22,3 +25,138 @@ def test_get_invalid_setting(self): self.assertIn("not a valid user variable attribute", str(context.exception)) + +class TestSettingsFile(unittest.TestCase): + """Test settings file is being used when provided""" + def test_import_yaml(self): + """Testing YAML Import""" + settings.load_settings_from_file("user_variables.yaml") + self.assertEqual(settings.get_setting("LOG_LEVEL"), "DEBUG") + def test_import_json(self): + """Testing JSON Import""" + settings.load_settings_from_file("user_variables.json") + self.assertEqual(settings.get_setting("LOG_LEVEL"), "WARNING") + def test_import_multi(self): + """Ensure Latest user_variables file takes precedent""" + settings.load_settings_from_file("user_variables.yaml") + settings.load_settings_from_file("user_variables.json") + self.assertEqual(settings.get_setting("LOG_LEVEL"), "WARNING") + self.assertEqual(settings.get_setting("DEFAULT_TIMEZONE"),"America/Chicago") + +class TestClusterSettings(unittest.TestCase): + """Test Cluster Settings""" + def test_get_cluster_dict_with_dict(self): + """Test retreiving cluster dictionary by passing in a dict""" + + request_cluster = IMPORTED_SETTINGS['FULL_SET']['mockserver1'] + response_cluster = settings.get_cluster_dict(request_cluster) + self.assertEqual(request_cluster, response_cluster) + def test_get_cluster_dict_with_str(self): + """Test retreiving cluster dictionary by passing in a dict""" + response_cluster = settings.get_cluster_dict("mockserver1") + self.assertEqual(IMPORTED_SETTINGS['FULL_SET']['mockserver1'], response_cluster) + def test_cluster_creation_minimal(self): + """Test cluster creation programatically""" + new_cluster = settings.create_cluster("cluster", "test.site") + expected_cluster = { + "url": "test.site", + 'tenant': {}, + 'api_token': {}, + 'verify_ssl': True, + 'is_managed': True, + } + self.assertEqual(new_cluster, expected_cluster) + def test_cluster_creation_complete(self): + """Test cluster creation programatically""" + new_cluster = settings.create_cluster( + "cluster", + "test.site", + tenant_ids={"tenant1":"tenant1-id"}, + tenant_tokens={"tenant1":"tenant1-token"}, + cluster_token="cluster_api_token" + ) + expected_cluster = { + "url": "test.site", + "tenant": { + "tenant1": "tenant1-id" + }, + "api_token": { + "tenant1": "tenant1-token" + }, + "verify_ssl": True, + "is_managed": True, + "cluster_token": "cluster_api_token" + } + self.assertEqual(new_cluster, expected_cluster) + def test_add_tenant_to_cluster(self): + """Test adding tenant to cluster programatically""" + new_cluster = settings.create_cluster( + "cluster", + "test.site" + ) + new_cluster = settings.add_tenant_to_cluster( + "cluster", + "new-id-here", + "new tenant_token", + "tenant2" + ) + expected_cluster = { + "url": "test.site", + "tenant": { + "tenant2": "new-id-here" + }, + "api_token": { + "tenant2": "new tenant_token" + }, + "verify_ssl": True, + "is_managed": True + } + self.assertEqual(new_cluster, expected_cluster) + +class TestClusterExceptions(unittest.TestCase): + """Exceptions with Cluster Operations""" + def test_cluster_creation_improper_tenant(self): + """Create cluster with an invalid tenant_id combination""" + with self.assertRaises(ValueError) as context: + settings.create_cluster( + "cluster", + "test.site", + tenant_ids={"tenant1":"tenant1-id"}, + cluster_token="cluster_api_token" + ) + self.assertTrue("Tenant and tenant token must both be dict" in str(context.exception)) + def test_add_tenant_to_cluster_dict(self): + """Addiing Tenant to Cluster Dictionary""" + with self.assertRaises(NotImplementedError) as context: + new_cluster = settings.create_cluster( + "cluster", + "test.site" + ) + new_cluster = settings.add_tenant_to_cluster( + new_cluster, + "new-id-here", + "new tenant_token", + "tenant2" + ) + self.assertTrue( + "Cluster dicts are not supported yet. Please use str for the cluster's key" \ + in str(context.exception)) + def test_add_tenant_to_cluster_nonexistant(self): + """Get Cluster Dict with an invalid/nonexistant key""" + with self.assertRaises(KeyError) as context: + settings.create_cluster( + "cluster", + "test.site" + ) + settings.add_tenant_to_cluster( + "cluster2", + "new-id-here", + "new tenant_token", + "tenant2" + ) + self.assertTrue("Cluster not found" in str(context.exception)) + def test_get_cluster_dict_nonexistant(self): + """Get Cluster Dict with an invalid/nonexistant key""" + with self.assertRaises(ValueError) as context: + settings.get_cluster_dict("cluster2") + self.assertTrue("Cluster not found" in str(context.exception)) From bb5f5c40fcc008315f49d601eefa764f208e6d24 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 21 Feb 2021 18:39:35 -0500 Subject: [PATCH 188/204] PAF-44 Forgot to add CircleCI Adjustments --- .circleci/config.yml | 1 + .circleci/mockserver.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index c4125ff..9dd3003 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,6 +16,7 @@ jobs: - run: command: | cp .circleci/mockserver.py user_variables.py + cp .circleci/user_variables.yaml user_variables.yaml pip install pipenv pipenv install --dev - run: diff --git a/.circleci/mockserver.py b/.circleci/mockserver.py index e53b848..51c2de3 100644 --- a/.circleci/mockserver.py +++ b/.circleci/mockserver.py @@ -14,6 +14,7 @@ } } +LOG_OUTPUT = "FILE" LOG_LEVEL = "INFO" # ROLE TYPE KEYS From b47f7b49cbf23a2fa06bbfe357387ec7f0138771 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 21 Feb 2021 18:41:50 -0500 Subject: [PATCH 189/204] Adding JSON files to test and updating config --- .circleci/config.yml | 3 ++- .circleci/mockserver.json | 14 ++++++++++++++ .circleci/mockserver.yaml | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 .circleci/mockserver.json create mode 100644 .circleci/mockserver.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml index 9dd3003..bf0d5f9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -16,7 +16,8 @@ jobs: - run: command: | cp .circleci/mockserver.py user_variables.py - cp .circleci/user_variables.yaml user_variables.yaml + cp .circleci/mockserver.yaml user_variables.yaml + cp .circleci/mockserver.json user_variables.json pip install pipenv pipenv install --dev - run: diff --git a/.circleci/mockserver.json b/.circleci/mockserver.json new file mode 100644 index 0000000..fc793dc --- /dev/null +++ b/.circleci/mockserver.json @@ -0,0 +1,14 @@ +{ + "FULL_SET": { + "mockserver1": { + "url": "veteran.philly:1080", + "tenant": { + "tenant1": "mockenv" + }, + "verify_ssl": false, + "is_managed": true, + "cluster_token": "cluster_token_here" + } + }, + "LOG_LEVEL": "WARNING" +} \ No newline at end of file diff --git a/.circleci/mockserver.yaml b/.circleci/mockserver.yaml new file mode 100644 index 0000000..251cf7a --- /dev/null +++ b/.circleci/mockserver.yaml @@ -0,0 +1,36 @@ +FULL_SET: + mockserver1: + url: veteran.philly:1080 + tenant: + tenant1: mockenv + api_token: + tenant1: "sample_api_token" + verify_ssl: False + is_managed: True + cluster_token: "Required for Cluster Operations in Managed" + + +LOG_ENABLED: True +LOG_LEVEL: "DEBUG" +LOG_OUTPUT: "FILE" + +# ROLE TYPE KEYS +# access_env +# change_settings +# install_agent +# view_logs +# view_senstive +# change_sensitive + +USER_GROUPS: + role_types: + access_env: accessenv + change_settings: changesettings + view_logs: logviewer + view_sensitive: viewsensitive + role_tenants: + - nonprod + - prod + +USER_GROUP_TEMPLATE: prefix_{USER_TYPE}_{TENANT}_{APP_NAME}_suffix +DEFAULT_TIMEZONE: America/Chicago From 47f4e7a6506f713737a180e046dd013c8e61bb4a Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 21 Feb 2021 18:44:02 -0500 Subject: [PATCH 190/204] PAF-44 #Fixing cluster url --- .circleci/mockserver.json | 2 +- .circleci/mockserver.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/mockserver.json b/.circleci/mockserver.json index fc793dc..f6b7389 100644 --- a/.circleci/mockserver.json +++ b/.circleci/mockserver.json @@ -1,7 +1,7 @@ { "FULL_SET": { "mockserver1": { - "url": "veteran.philly:1080", + "url": "mockserver.philly:1080", "tenant": { "tenant1": "mockenv" }, diff --git a/.circleci/mockserver.yaml b/.circleci/mockserver.yaml index 251cf7a..91212c8 100644 --- a/.circleci/mockserver.yaml +++ b/.circleci/mockserver.yaml @@ -1,6 +1,6 @@ FULL_SET: mockserver1: - url: veteran.philly:1080 + url: mockserver:1080 tenant: tenant1: mockenv api_token: From dd4ce8d045ef440f0882a742dcd2351566e6f235 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 21 Feb 2021 19:22:11 -0500 Subject: [PATCH 191/204] PAF-44 #Fixing Code Smells --- dynatrace/framework/settings.py | 4 +- tests/special_test_settings_no_variables.py | 10 +++-- tests/test_settings.py | 45 ++++++++++++--------- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/dynatrace/framework/settings.py b/dynatrace/framework/settings.py index 1f672bf..8bccf11 100644 --- a/dynatrace/framework/settings.py +++ b/dynatrace/framework/settings.py @@ -114,9 +114,7 @@ def create_cluster(cluster_name, url, **kwargs): for tenant in tenant_ids: cluster['tenant'][tenant] = tenant_ids [tenant] cluster['api_token'][tenant] = tenant_tokens [tenant] - elif tenant_ids is None and tenant_tokens is None: - pass - else: + elif not(tenant_ids is None and tenant_tokens is None): raise ValueError("Tenant and tenant token must both be dict") if 'FULL_SET' not in __IMPORTED_SETTINGS__: diff --git a/tests/special_test_settings_no_variables.py b/tests/special_test_settings_no_variables.py index 6e58d70..0a6bdff 100644 --- a/tests/special_test_settings_no_variables.py +++ b/tests/special_test_settings_no_variables.py @@ -2,18 +2,20 @@ import unittest from os import rename +USER_VARIABLES_CURRENT="user_variables.py" +USER_VARIABLES_MOVED="user_variables.py.tmp" class TestSettingsWithoutVarFile(unittest.TestCase): """Special test cases when user_variables is absent""" def test_settings_without_var_file(self): """Test should return default value when user_variables missing""" - rename("user_variables.py", "user_variables.py.tmp") + rename(USER_VARIABLES_CURRENT, USER_VARIABLES_MOVED) from dynatrace.framework import settings # pylint: disable=import-outside-toplevel timezone = settings.get_setting("DEFAULT_TIMEZONE") self.assertEqual(timezone, "UTC") - rename("user_variables.py.tmp", "user_variables.py") + rename(USER_VARIABLES_MOVED, USER_VARIABLES_CURRENT) def test_cluster_creation_from_nothing(self): """Create Cluster without a preexisting user_variables""" - rename("user_variables.py", "user_variables.py.tmp") + rename(USER_VARIABLES_CURRENT, USER_VARIABLES_MOVED) from dynatrace.framework import settings # pylint: disable=import-outside-toplevel new_cluster = settings.create_cluster("cluster", "test.site") expected_cluster = { @@ -24,4 +26,4 @@ def test_cluster_creation_from_nothing(self): 'is_managed': True, } self.assertEqual(new_cluster, expected_cluster) - rename("user_variables.py.tmp", "user_variables.py") + rename(USER_VARIABLES_MOVED, USER_VARIABLES_CURRENT) diff --git a/tests/test_settings.py b/tests/test_settings.py index 33f8679..598b537 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -3,9 +3,14 @@ import yaml from dynatrace.framework import settings -SETTINGS_FILE = "user_variables.yaml" -with open(SETTINGS_FILE) as file: +SETTINGS_JSON = "user_variables.json" +SETTINGS_YAML = "user_variables.yaml" +with open(SETTINGS_YAML) as file: IMPORTED_SETTINGS = yaml.load(file, Loader=yaml.FullLoader) + +URL = "test.site" +TENANT_TOKEN = "new_tenant_token" + class TestSettings(unittest.TestCase): """Standard Testing Class""" def test_get_setting_from_user_variable(self): @@ -30,16 +35,16 @@ class TestSettingsFile(unittest.TestCase): """Test settings file is being used when provided""" def test_import_yaml(self): """Testing YAML Import""" - settings.load_settings_from_file("user_variables.yaml") + settings.load_settings_from_file(SETTINGS_YAML) self.assertEqual(settings.get_setting("LOG_LEVEL"), "DEBUG") def test_import_json(self): """Testing JSON Import""" - settings.load_settings_from_file("user_variables.json") + settings.load_settings_from_file(SETTINGS_JSON) self.assertEqual(settings.get_setting("LOG_LEVEL"), "WARNING") def test_import_multi(self): """Ensure Latest user_variables file takes precedent""" - settings.load_settings_from_file("user_variables.yaml") - settings.load_settings_from_file("user_variables.json") + settings.load_settings_from_file(SETTINGS_YAML) + settings.load_settings_from_file(SETTINGS_JSON) self.assertEqual(settings.get_setting("LOG_LEVEL"), "WARNING") self.assertEqual(settings.get_setting("DEFAULT_TIMEZONE"),"America/Chicago") @@ -57,9 +62,9 @@ def test_get_cluster_dict_with_str(self): self.assertEqual(IMPORTED_SETTINGS['FULL_SET']['mockserver1'], response_cluster) def test_cluster_creation_minimal(self): """Test cluster creation programatically""" - new_cluster = settings.create_cluster("cluster", "test.site") + new_cluster = settings.create_cluster("cluster", URL) expected_cluster = { - "url": "test.site", + "url": URL, 'tenant': {}, 'api_token': {}, 'verify_ssl': True, @@ -70,13 +75,13 @@ def test_cluster_creation_complete(self): """Test cluster creation programatically""" new_cluster = settings.create_cluster( "cluster", - "test.site", + URL, tenant_ids={"tenant1":"tenant1-id"}, tenant_tokens={"tenant1":"tenant1-token"}, cluster_token="cluster_api_token" ) expected_cluster = { - "url": "test.site", + "url": URL, "tenant": { "tenant1": "tenant1-id" }, @@ -90,23 +95,23 @@ def test_cluster_creation_complete(self): self.assertEqual(new_cluster, expected_cluster) def test_add_tenant_to_cluster(self): """Test adding tenant to cluster programatically""" - new_cluster = settings.create_cluster( + settings.create_cluster( "cluster", - "test.site" + URL ) new_cluster = settings.add_tenant_to_cluster( "cluster", "new-id-here", - "new tenant_token", + TENANT_TOKEN, "tenant2" ) expected_cluster = { - "url": "test.site", + "url": URL, "tenant": { "tenant2": "new-id-here" }, "api_token": { - "tenant2": "new tenant_token" + "tenant2": TENANT_TOKEN }, "verify_ssl": True, "is_managed": True @@ -120,7 +125,7 @@ def test_cluster_creation_improper_tenant(self): with self.assertRaises(ValueError) as context: settings.create_cluster( "cluster", - "test.site", + URL, tenant_ids={"tenant1":"tenant1-id"}, cluster_token="cluster_api_token" ) @@ -130,12 +135,12 @@ def test_add_tenant_to_cluster_dict(self): with self.assertRaises(NotImplementedError) as context: new_cluster = settings.create_cluster( "cluster", - "test.site" + URL ) new_cluster = settings.add_tenant_to_cluster( new_cluster, "new-id-here", - "new tenant_token", + TENANT_TOKEN, "tenant2" ) self.assertTrue( @@ -146,12 +151,12 @@ def test_add_tenant_to_cluster_nonexistant(self): with self.assertRaises(KeyError) as context: settings.create_cluster( "cluster", - "test.site" + URL ) settings.add_tenant_to_cluster( "cluster2", "new-id-here", - "new tenant_token", + TENANT_TOKEN, "tenant2" ) self.assertTrue("Cluster not found" in str(context.exception)) From 7abcca7e0ccdfdbb2d40f50f40feaf3f7bf28e85 Mon Sep 17 00:00:00 2001 From: Aaron Date: Sun, 21 Feb 2021 22:16:09 -0500 Subject: [PATCH 192/204] PAF-44 #Adapting request_handler for str cluster --- dynatrace/framework/request_handler.py | 39 ++++++++++++++------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index ca4c0f4..78695af 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -2,12 +2,12 @@ from enum import Enum, auto import time import functools -import requests from copy import deepcopy +import requests from dynatrace.framework import logging +from dynatrace.framework.settings import get_cluster_dict from dynatrace.framework.exceptions import InvalidAPIResponseException, ManagedClusterOnlyException - requests.packages.urllib3.disable_warnings() # pylint: disable=no-member logger = logging.get_logger(__name__) @@ -139,22 +139,24 @@ def make_api_call(cluster, endpoint, tenant=None, method=HTTP.GET, **kwargs): \n @returns - response from request\n """ + cluster_dict = get_cluster_dict(cluster) + # cluster_dict = cluster # Set the right URL for the operation - url = f"{generate_tenant_url(cluster, tenant)}{endpoint}" \ - if tenant else f"{HTTPS_STR}{cluster['url']}{endpoint}" + url = f"{generate_tenant_url(cluster_dict, tenant)}{endpoint}" \ + if tenant else f"{HTTPS_STR}{cluster_dict['url']}{endpoint}" logger.debug("URL used for API call: %s", url) # Get correct token for the operation if 'onpremise' in str(endpoint) or 'cluster' in str(endpoint): - check_managed(cluster) - headers = dict(Authorization=f"Api-Token {cluster['cluster_token']}") + check_managed(cluster_dict) + headers = dict(Authorization=f"Api-Token {cluster_dict['cluster_token']}") else: - headers = dict(Authorization=f"Api-Token {cluster['api_token'][tenant]}") + headers = dict(Authorization=f"Api-Token {cluster_dict['api_token'][tenant]}") logger.debug("API call details:") call_details = deepcopy(locals()) - call_details["cluster"]["api_token"][tenant] = "*" * 12 - call_details["cluster"]["cluster_token"] = "*" * 12 + del call_details["cluster_dict"]["api_token"] # Remove API Tokens for all tenants + call_details["cluster_dict"]["cluster_token"] = "*" * 12 call_details["headers"]["Authorization"] = "*" * 12 logger.debug(call_details) # Loop to retry in case of rate limits @@ -163,7 +165,7 @@ def make_api_call(cluster, endpoint, tenant=None, method=HTTP.GET, **kwargs): method=str(method), url=url, headers=headers, - verify=cluster.get('verify_ssl'), + verify=cluster_dict.get('verify_ssl'), **kwargs ) if check_response(response): @@ -171,7 +173,6 @@ def make_api_call(cluster, endpoint, tenant=None, method=HTTP.GET, **kwargs): return response - def get_results_whole(cluster, tenant, endpoint, api_version, **kwargs): """Gets a multi-paged result set and returns it whole. \n @@ -206,7 +207,7 @@ def get_results_whole(cluster, tenant, endpoint, api_version, **kwargs): while cursor: if cursor != 1: - logger.debug("Getting next page of results. Cursor is %", cursor) + logger.debug("Getting next page of results. Cursor is %s", cursor) if not is_v2 or endpoint == TenantAPIs.ONEAGENTS: # V1 and OneAgents require all other query params are preserved kwargs['nextPageKey'] = cursor @@ -304,7 +305,7 @@ def check_response(response): headers = response.headers if response.status_code == 429: - logger.warn( + logger.warning( "Endpoint request limit of %s was reached!", headers['x-ratelimit-limit'] ) # Wait until the limit resets and try again @@ -312,7 +313,7 @@ def check_response(response): # Check that there's actually time to wait if time_to_wait > 0: - logger.warn("Waiting %s sec until the limit resets.", time_to_wait) + logger.warning("Waiting %s sec until the limit resets.", time_to_wait) time.sleep(float(time_to_wait)) return False @@ -332,7 +333,8 @@ def check_response(response): def check_managed(cluster): """Checks if the Cluster Operation is valid (Managed) for the current cluster""" logger.debug("Checking that the cluster is Managed.") - if not cluster['is_managed']: + cluster_dict = get_cluster_dict(cluster) + if not cluster_dict['is_managed']: try: raise ManagedClusterOnlyException() except ManagedClusterOnlyException: @@ -345,11 +347,12 @@ def check_managed(cluster): def generate_tenant_url(cluster, tenant): """Generate URL based on SaaS or Managed""" url = HTTPS_STR - if cluster["is_managed"]: + cluster_dict = get_cluster_dict(cluster) + if cluster_dict["is_managed"]: logger.debug("Generating URL for a Managed cluster.") - url += cluster['url'] + "/e/" + cluster['tenant'][tenant] + url += cluster_dict['url'] + "/e/" + cluster_dict['tenant'][tenant] else: logger.debug("Generating URL for a SaaS cluster.") - url += cluster['tenant'][tenant] + "." + cluster['url'] + url += cluster_dict['tenant'][tenant] + "." + cluster_dict['url'] return url From 0f462792b1d57b755050b30f75a2dbb2960fb848 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 00:09:11 -0500 Subject: [PATCH 193/204] PAF-84 #Linting Cleanup Recommended --- .circleci/mockserver.yaml | 1 + .github/workflows/linter.yml | 1 + dynatrace/framework/settings.py | 2 ++ scripts/template.py | 5 +++-- 4 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.circleci/mockserver.yaml b/.circleci/mockserver.yaml index 91212c8..96e95e3 100644 --- a/.circleci/mockserver.yaml +++ b/.circleci/mockserver.yaml @@ -1,3 +1,4 @@ +--- FULL_SET: mockserver1: url: mockserver:1080 diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index ef2be6c..2aa9f2d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,3 +1,4 @@ +--- ########################### ########################### ## Linter GitHub Actions ## diff --git a/dynatrace/framework/settings.py b/dynatrace/framework/settings.py index 8bccf11..870252c 100644 --- a/dynatrace/framework/settings.py +++ b/dynatrace/framework/settings.py @@ -136,6 +136,8 @@ def add_tenant_to_cluster(cluster, tenant_id, tenant_token, tenant_name): raise KeyError("Cluster not found") return __IMPORTED_SETTINGS__['FULL_SET'][cluster] + raise ValueError("Tenant and tenant token must both be dict") + def load_settings_from_file(settings_file): """Assign setting value)s as defined by the cluster""" global __IMPORTED_SETTINGS__ # pylint: disable=global-statement diff --git a/scripts/template.py b/scripts/template.py index 168724c..f108203 100644 --- a/scripts/template.py +++ b/scripts/template.py @@ -1,4 +1,5 @@ +"""Template to make your own scripts""" # IMPORTS GO HERE -import change_pythonpath # Must be first import +import change_pythonpath # Must be first import pylint: disable=unused-import -# YOUR SCRIPT GOES HERE \ No newline at end of file +# YOUR SCRIPT GOES HERE From 9af27772d27a8e31fbf37980d858b7cbaf9e685a Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 00:12:57 -0500 Subject: [PATCH 194/204] PAF-84 #Attempting to fix linter on git --- .github/workflows/linter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 2aa9f2d..091107d 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -48,7 +48,7 @@ jobs: - name: Lint Code Base uses: github/super-linter@v3.11.0 env: - VALIDATE_ALL_CODEBASE: false + VALIDATE_ALL_CODEBASE: true DEFAULT_BRANCH: master LINTER_RULES_PATH: . PYTHON_PYLINT_CONFIG_FILE: .pylintrc From a76bf9b0c66ebb47d3bada946f80d0eeca9eea71 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 00:34:04 -0500 Subject: [PATCH 195/204] PAF-84 #Refactoring logging to log_handler --- .circleci/config.yml | 1 + dynatrace/framework/{logging.py => log_handler.py} | 0 dynatrace/framework/request_handler.py | 4 ++-- dynatrace/tenant/entities.py | 4 ++-- dynatrace/tenant/extensions.py | 4 ++-- dynatrace/tenant/management_zones.py | 4 ++-- dynatrace/tenant/metrics.py | 4 ++-- dynatrace/tenant/oneagents.py | 4 ++-- dynatrace/tenant/problems.py | 4 ++-- dynatrace/tenant/request_attributes.py | 6 +++--- 10 files changed, 18 insertions(+), 17 deletions(-) rename dynatrace/framework/{logging.py => log_handler.py} (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index bf0d5f9..1f833e9 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,3 +1,4 @@ +--- version: 2 jobs: build: diff --git a/dynatrace/framework/logging.py b/dynatrace/framework/log_handler.py similarity index 100% rename from dynatrace/framework/logging.py rename to dynatrace/framework/log_handler.py diff --git a/dynatrace/framework/request_handler.py b/dynatrace/framework/request_handler.py index 78695af..136f9d1 100644 --- a/dynatrace/framework/request_handler.py +++ b/dynatrace/framework/request_handler.py @@ -4,12 +4,12 @@ import functools from copy import deepcopy import requests -from dynatrace.framework import logging +from dynatrace.framework import log_handler from dynatrace.framework.settings import get_cluster_dict from dynatrace.framework.exceptions import InvalidAPIResponseException, ManagedClusterOnlyException requests.packages.urllib3.disable_warnings() # pylint: disable=no-member -logger = logging.get_logger(__name__) +logger = log_handler.get_logger(__name__) HTTPS_STR = "https://" diff --git a/dynatrace/tenant/entities.py b/dynatrace/tenant/entities.py index 0353768..9dff064 100644 --- a/dynatrace/tenant/entities.py +++ b/dynatrace/tenant/entities.py @@ -1,10 +1,10 @@ """Module for Entities API operations""" from enum import Enum, auto -from dynatrace.framework import logging +from dynatrace.framework import log_handler from dynatrace.framework import request_handler as rh -logger = logging.get_logger(__name__) +logger = log_handler.get_logger(__name__) class EntityTypes(Enum): diff --git a/dynatrace/tenant/extensions.py b/dynatrace/tenant/extensions.py index e32d9cf..6cb7186 100644 --- a/dynatrace/tenant/extensions.py +++ b/dynatrace/tenant/extensions.py @@ -1,9 +1,9 @@ """Operations Interacting with Dynatrace Extensions API""" -from dynatrace.framework import request_handler as rh, logging +from dynatrace.framework import request_handler as rh, log_handler from dynatrace.tenant import metrics ENDPOINT = rh.TenantAPIs.EXTENSIONS -logger = logging.get_logger(__name__) +logger = log_handler.get_logger(__name__) def get_all_extensions(cluster, tenant, page_size=200): diff --git a/dynatrace/tenant/management_zones.py b/dynatrace/tenant/management_zones.py index 24d78bd..1f99cab 100644 --- a/dynatrace/tenant/management_zones.py +++ b/dynatrace/tenant/management_zones.py @@ -2,10 +2,10 @@ import json from enum import Enum, auto -from dynatrace.framework import request_handler as rh, logging +from dynatrace.framework import request_handler as rh, log_handler ENDPOINT = str(rh.TenantAPIs.MANAGEMENT_ZONES) -logger = logging.get_logger(__name__) +logger = log_handler.get_logger(__name__) class RuleTypes(Enum): diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index 52d75a3..d0591ac 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -1,9 +1,9 @@ """Module for interacting with the Metrics API""" -from dynatrace.framework import request_handler as rh, logging +from dynatrace.framework import request_handler as rh, log_handler from dynatrace.framework.exceptions import InvalidAPIResponseException ENDPOINT = str(rh.TenantAPIs.METRICS) -logger = logging.get_logger(__name__) +logger = log_handler.get_logger(__name__) def get_metric_descriptor(cluster, tenant, **kwargs): diff --git a/dynatrace/tenant/oneagents.py b/dynatrace/tenant/oneagents.py index d889f5e..98ab683 100644 --- a/dynatrace/tenant/oneagents.py +++ b/dynatrace/tenant/oneagents.py @@ -1,8 +1,8 @@ """Module for OneAgent operations.""" -from dynatrace.framework import request_handler as rh, logging +from dynatrace.framework import request_handler as rh, log_handler -logger = logging.get_logger(__name__) +logger = log_handler.get_logger(__name__) def get_host_units_tenantwide(cluster, tenant, **kwargs): diff --git a/dynatrace/tenant/problems.py b/dynatrace/tenant/problems.py index bc78745..3192115 100644 --- a/dynatrace/tenant/problems.py +++ b/dynatrace/tenant/problems.py @@ -1,8 +1,8 @@ """Module for interactions with the Problems (V2) API""" -from dynatrace.framework import request_handler as rh, logging +from dynatrace.framework import request_handler as rh, log_handler ENDPOINT = str(rh.TenantAPIs.PROBLEMS) -logger = logging.get_logger(__name__) +logger = log_handler.get_logger(__name__) def get_all_problems(cluster, tenant, **kwargs): diff --git a/dynatrace/tenant/request_attributes.py b/dynatrace/tenant/request_attributes.py index ac8cfc6..1b53444 100644 --- a/dynatrace/tenant/request_attributes.py +++ b/dynatrace/tenant/request_attributes.py @@ -2,10 +2,10 @@ import os import json -from dynatrace.framework import request_handler as rh, logging +from dynatrace.framework import request_handler as rh, log_handler ENDPOINT = str(rh.TenantAPIs.REQUEST_ATTRIBUTES) -logger = logging.get_logger(__name__) +logger = log_handler.get_logger(__name__) def get_all_request_attributes(cluster, tenant): @@ -120,7 +120,7 @@ def delete_request_attribute_by_name(cluster, tenant, ra_name): \n @param cluster (dict) - Dynatrace Cluster dictionary, as taken from variable set\n @param tenant (str) - Dynatrace Tenant name, as taken from variable set\n - @param ra_name (str) - name of the request attribute to delete + @param ra_name (str) - name of the request attribute to delete\n \n @returns Response - HTTP Response \n From 1496a6ca9925278d5df9e1da38962919b8b6326b Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 00:41:14 -0500 Subject: [PATCH 196/204] PAF-84 #Fixing warn to warning in logs --- dynatrace/tenant/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dynatrace/tenant/metrics.py b/dynatrace/tenant/metrics.py index d0591ac..d46302a 100644 --- a/dynatrace/tenant/metrics.py +++ b/dynatrace/tenant/metrics.py @@ -80,7 +80,7 @@ def get_metric_data(cluster, tenant, **kwargs): params=kwargs) except InvalidAPIResponseException as err: if 'metric key that could not be resolved in the metric registry' in str(err): - logger.warn("Invalid metric ID encountered. Returning results so far.") + logger.warning("Invalid metric ID encountered. Returning results so far.") break logger.exception("Error: Invalid API response", stack_info=True) raise From 4a86873146e07ecea25daeabb62f968bcd75ef8c Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 01:01:03 -0500 Subject: [PATCH 197/204] PAF-84 #Linting Tests --- tests/test_extensions.py | 2 +- tests/test_request_attributes.py | 17 +++++++++-------- tests/test_request_naming.py | 3 ++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/test_extensions.py b/tests/test_extensions.py index 25de504..12b00d2 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -151,7 +151,7 @@ def test_get_extension_metrics(self): result = extensions.get_extension_metrics(CLUSTER, TENANT, EXTENSION_ID) expected_result = list( - m.get('metricId') + m.get('metricId') for m in testtools.expected_payload(response_file).get('metrics') ) diff --git a/tests/test_request_attributes.py b/tests/test_request_attributes.py index f83c6a4..0cf91f1 100644 --- a/tests/test_request_attributes.py +++ b/tests/test_request_attributes.py @@ -19,7 +19,7 @@ class TestRequestAttributes(unittest.TestCase): """Test cases for main functionality of the request_attributes module""" - def test_create_or_update_request_attribute_U(self): + def test_create_or_update_request_attribute_u(self): """Tests the create_or_update_request_attribute function. Test the update portion of this function. """ @@ -53,7 +53,7 @@ def test_create_or_update_request_attribute_U(self): self.assertEqual(result, expected_result) - def test_create_or_update_request_attribute_C(self): + def test_create_or_update_request_attribute_c(self): """Tests the create_or_update_request_attribute function. Test the create portion of this function. """ @@ -266,12 +266,13 @@ def test_update_request_attribute(self): class TestErrorHandling(unittest.TestCase): - def test_delete_request_attribute_by_name_RuntimeError(self): + """Tests Request Attribute Error Handling""" + def test_delete_request_attribute_by_name_runtime_error(self): """Tests error handling for function delete_request_attribute_by_name. RuntimeError should be raised when request attribute ID is not found. """ response_file = f"{RESPONSE_DIR}/get_all.json" - RA_NAME = "invalid_name" + ra_name = "invalid_name" testtools.create_mockserver_expectation( cluster=CLUSTER, @@ -283,16 +284,16 @@ def test_delete_request_attribute_by_name_RuntimeError(self): ) with self.assertRaises(RuntimeError): - request_attributes.delete_request_attribute_by_name(CLUSTER, TENANT, RA_NAME) + request_attributes.delete_request_attribute_by_name(CLUSTER, TENANT, ra_name) - def test_export_to_files_RuntimeError(self): + def test_export_to_files_runtime_error(self): """Tests error handling for function export_to_files. RuntimeError should be raised when export folder does not exist. """ - FOLDER = "invalid_folder/path" + folder = "invalid_folder/path" with self.assertRaises(RuntimeError): - request_attributes.export_to_files(CLUSTER, TENANT, FOLDER) + request_attributes.export_to_files(CLUSTER, TENANT, folder) if __name__ == "__main__": diff --git a/tests/test_request_naming.py b/tests/test_request_naming.py index 1aa7385..b3149b6 100644 --- a/tests/test_request_naming.py +++ b/tests/test_request_naming.py @@ -148,7 +148,8 @@ def test_update_naming_rule(self): class TestErrorHandling(unittest.TestCase): - def test_export_to_files_RuntimeError(self): + """Test Request Naming Error Handling""" + def test_export_to_files_runtime_error(self): """Tests error handling for function export_to_files. RuntimeError should be raised when the folder path does not exist. """ From fc778cad0b6e070b0135edc7b15ca11bcbf7a75f Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 01:10:25 -0500 Subject: [PATCH 198/204] PAF-84 #Linting --- dynatrace/tenant/maintenance.py | 4 ++-- tests/test_maintenance_windows.py | 4 ++-- tests/test_request_attributes.py | 4 ++-- tests/test_request_naming.py | 4 ++-- tests/test_timeseries.py | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/dynatrace/tenant/maintenance.py b/dynatrace/tenant/maintenance.py index e6d4600..7e92a1a 100644 --- a/dynatrace/tenant/maintenance.py +++ b/dynatrace/tenant/maintenance.py @@ -3,7 +3,7 @@ import re from enum import Enum, auto import dynatrace.framework.request_handler as rh -import user_variables +from dynatrace.framework.settings import get_setting from dynatrace.framework.exceptions import InvalidDateFormatException @@ -338,7 +338,7 @@ def generate_schedule( } if zone_id is None: - schedule['zoneId'] = user_variables.DEFAULT_TIMEZONE + schedule['zoneId'] = get_setting('DEFAULT_TIMEZONE') if recurrence_type != "ONCE": # Check Start Time diff --git a/tests/test_maintenance_windows.py b/tests/test_maintenance_windows.py index e1ac56a..34acb0a 100644 --- a/tests/test_maintenance_windows.py +++ b/tests/test_maintenance_windows.py @@ -1,12 +1,12 @@ """Test Cases For Maintenance Windows.""" import unittest -import user_variables # pylint: disable=import-error from tests import tooling_for_test from dynatrace.tenant import maintenance from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.framework.settings import get_cluster_dict from dynatrace.framework.exceptions import InvalidDateFormatException -CLUSTER = user_variables.FULL_SET["mockserver1"] +CLUSTER = get_cluster_dict("mockserver1") TENANT = "tenant1" URL_PATH = str(TenantAPIs.MAINTENANCE_WINDOWS) TEST_RANGE_START = "2020-01-01 00:00" diff --git a/tests/test_request_attributes.py b/tests/test_request_attributes.py index 0cf91f1..9ec5039 100644 --- a/tests/test_request_attributes.py +++ b/tests/test_request_attributes.py @@ -3,11 +3,11 @@ import unittest import os import tests.tooling_for_test as testtools -from user_variables import FULL_SET from dynatrace.framework.request_handler import TenantAPIs, HTTP +from dynatrace.framework.settings import get_cluster_dict from dynatrace.tenant import request_attributes -CLUSTER = FULL_SET["mockserver1"] +CLUSTER = get_cluster_dict("mockserver1") TENANT = "tenant1" URL_PATH = str(TenantAPIs.REQUEST_ATTRIBUTES) RA_ID = "123abc456-a123-1234-4321-def123ghi45" diff --git a/tests/test_request_naming.py b/tests/test_request_naming.py index b3149b6..66db0c2 100644 --- a/tests/test_request_naming.py +++ b/tests/test_request_naming.py @@ -3,11 +3,11 @@ import unittest import os import tests.tooling_for_test as testtools -from user_variables import FULL_SET from dynatrace.framework.request_handler import TenantAPIs, HTTP +from dynatrace.framework.settings import get_cluster_dict from dynatrace.tenant import request_naming -CLUSTER = FULL_SET["mockserver1"] +CLUSTER = get_cluster_dict("mockserver1") TENANT = "tenant1" URL_PATH = str(TenantAPIs.REQUEST_NAMING) RULE_ID = "abc1234def-1233-3321-ab123-abc123defghi" diff --git a/tests/test_timeseries.py b/tests/test_timeseries.py index 97c096c..2891436 100644 --- a/tests/test_timeseries.py +++ b/tests/test_timeseries.py @@ -1,12 +1,12 @@ """Test cases for Timeseries (V1) API operations""" import unittest -from tests import tooling_for_test as testtools -from user_variables import FULL_SET -from dynatrace.tenant import timeseries from dynatrace.framework.request_handler import TenantAPIs, HTTP +from dynatrace.framework.settings import get_cluster_dict +from dynatrace.tenant import timeseries +from tests import tooling_for_test as testtools -CLUSTER = FULL_SET["mockserver1"] +CLUSTER = get_cluster_dict("mockserver1") TENANT = "tenant1" URL_PATH = str(TenantAPIs.TIMESERIES) TIMESERIES_ID = "custom.test.timeseries" From 6a49e63c8bb47e6261bee54dcbc64f33f26f195a Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 01:16:08 -0500 Subject: [PATCH 199/204] PAF-84 #Linting --- tests/test_management_zones.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_management_zones.py b/tests/test_management_zones.py index ba837cd..6060e54 100644 --- a/tests/test_management_zones.py +++ b/tests/test_management_zones.py @@ -2,11 +2,11 @@ import unittest import tests.tooling_for_test as testtools -from user_variables import FULL_SET from dynatrace.tenant import management_zones +from dynatrace.framework.settings import get_cluster_dict from dynatrace.framework.request_handler import TenantAPIs, HTTP -CLUSTER = FULL_SET["mockserver1"] +CLUSTER = get_cluster_dict("mockserver1") TENANT = "tenant1" URL_PATH = str(TenantAPIs.MANAGEMENT_ZONES) RESPONSE_DIR = "tests/mockserver_payloads/responses/management_zones" From aa04d735febf471fee59beec45642cc602ce8b98 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 01:19:13 -0500 Subject: [PATCH 200/204] PAF-84 #Removing func that should be standalone --- dynatrace/cluster/user_groups.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/dynatrace/cluster/user_groups.py b/dynatrace/cluster/user_groups.py index 6734da0..b0bdd4b 100644 --- a/dynatrace/cluster/user_groups.py +++ b/dynatrace/cluster/user_groups.py @@ -97,34 +97,3 @@ def delete_app_groups_setwide(app_name): for cluster in user_variables.FULL_SET.values(): if cluster['is_managed']: delete_app_groups(cluster, app_name) - - -def create_app_clusterwide(cluster, app_name, zones=None): - """Create App User Groups and Management Zones""" - # Create Standard App MZs - mz_list = {} - for tenant_key in cluster['tenant'].keys(): - mzh.add_management_zone( - cluster, - tenant_key, - str.upper(app_name) - ) - if tenant_key in zones: - mz_list[tenant_key] = [] - for zone in zones[tenant_key]: - mz_id = mzh.add_management_zone( - cluster, - tenant_key, - str.upper(app_name), - zone - ) - if mz_id is not None: - mz_list[tenant_key].append(mz_id) - - # Create User Groups - user_groups = create_app_groups(cluster, app_name) - print(user_groups) - - # for tenant in user_variables.USER_GROUPS['role_tenants']: - # if "access_env" in user_groups [tenant]: - # add_mz_to_user From cc8375d5dfade176f12ad13a8fe220a491f07c6a Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 01:23:48 -0500 Subject: [PATCH 201/204] PAF-84 #Linting --- dynatrace/cluster/user_groups.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dynatrace/cluster/user_groups.py b/dynatrace/cluster/user_groups.py index b0bdd4b..72db000 100644 --- a/dynatrace/cluster/user_groups.py +++ b/dynatrace/cluster/user_groups.py @@ -2,7 +2,6 @@ """Cluster Group Operations""" import user_variables # pylint: disable=import-error from dynatrace.framework import request_handler as rh -from dynatrace.tenant import management_zones as mzh MZ_USER_PERMISSONS = { "access_env": "VIEWER", From 441977dfa010949f99204a4845a07150c515f8c8 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 01:32:35 -0500 Subject: [PATCH 202/204] PAF-84 #Linting --- tests/test_metrics.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_metrics.py b/tests/test_metrics.py index 359902f..4be5bbc 100644 --- a/tests/test_metrics.py +++ b/tests/test_metrics.py @@ -2,13 +2,12 @@ Test Suite for Metrics API """ import unittest -# from user_variables import FULL_SET # pylint: disable=import-error -from user_variables import FULL_SET from tests import tooling_for_test as testtools from dynatrace.framework.request_handler import TenantAPIs +from dynatrace.framework.settings import get_cluster_dict from dynatrace.tenant import metrics -CLUSTER = FULL_SET["mockserver1"] +CLUSTER = get_cluster_dict("mockserver1") TENANT = "tenant1" URL_PATH = str(TenantAPIs.METRICS) METRIC_SELECTOR = 'builtin:host.mem.avail.*' From e1b761732bc28c80e744531003a84f77892145f3 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 09:46:22 -0500 Subject: [PATCH 203/204] PAF-84 #Trying detailed on linter --- .github/workflows/linter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 091107d..612cdc6 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -56,6 +56,7 @@ jobs: LOG_FILE: super-linter.log OUTPUT_FOLDER: super-linter.report OUTPUT_FORMAT: tap + OUTPUT_DETAILS: detailed VALIDATE_YAML: true VALIDATE_JSON: true VALIDATE_EDITORCONFIG: true From 7cd27bd4f79f55e60b1492cbde2f8d77a11885c0 Mon Sep 17 00:00:00 2001 From: Aaron Date: Mon, 22 Feb 2021 10:07:59 -0500 Subject: [PATCH 204/204] #PAF-84 Markdown Linting --- Documentation/INSTALLATION.md | 2 +- Documentation/PROJECT_OBJECTIVES.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/INSTALLATION.md b/Documentation/INSTALLATION.md index 101c9a6..b06fd44 100644 --- a/Documentation/INSTALLATION.md +++ b/Documentation/INSTALLATION.md @@ -12,7 +12,7 @@ If you see a TODO for something related, please reach out to Aaron Philipose on ### Install indepentantly - [Download the latest version of Python 3](https://www.python.org/downloads/) -- Run the installer +- Run the installer - Use default options. - Select to add PYTHON_HOME to PATH - Open PowerShell or Command Line diff --git a/Documentation/PROJECT_OBJECTIVES.md b/Documentation/PROJECT_OBJECTIVES.md index 76c7ff7..e60bcc0 100644 --- a/Documentation/PROJECT_OBJECTIVES.md +++ b/Documentation/PROJECT_OBJECTIVES.md @@ -1,5 +1,5 @@ # Project Objectives -1. Lower the barrier of entry of creating repeatable Dynatrace scripts +1. Lower the barrier of entry of creating repeatable Dynatrace scripts 2. Easy to Understand (Less time learning another new thing and more time using!) 3. Allow other applications to implement this as a foundation () \ No newline at end of file