From 8710e2a64145fcbcfbd79c3778bf7361f8b9e31e Mon Sep 17 00:00:00 2001 From: Adi Pandit <39402771+adiosspandit@users.noreply.github.com> Date: Thu, 16 Sep 2021 13:50:19 -0400 Subject: [PATCH] Updated fetch entities to fetch workloads and added script to override and reset workload golden signals (#33) * feat: add support for fetching workloads Added --workload flag to fetch workload entities * feat: Added script to override and reset workload golden signals Golden signals can be specified in a json file saved under ./goldensignals directory --- README.md | 39 ++++++++- fetchentities.py | 4 + goldensignals/linuxgoldensignals.json | 51 +++++++++++ goldensignals/windowsgoldensignals.json | 51 +++++++++++ library/clients/entityclient.py | 18 +++- library/clients/goldensignals.py | 54 ++++++++++++ library/clients/gql.py | 34 ++++++++ library/localstore.py | 17 +++- library/utils.py | 1 + wlgoldensignals.py | 109 ++++++++++++++++++++++++ 10 files changed, 373 insertions(+), 5 deletions(-) create mode 100644 goldensignals/linuxgoldensignals.json create mode 100644 goldensignals/windowsgoldensignals.json create mode 100644 library/clients/goldensignals.py create mode 100644 library/clients/gql.py create mode 100644 wlgoldensignals.py diff --git a/README.md b/README.md index 3ed9382..97f3d96 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ APM Settings ## Getting Started -The table below lists the scripts that can be used for different migration use cases. +The table below lists the scripts that can be used for different migration and other use cases. These scripts, in some cases, require sequential execution as they build the data necessary to migrate entities from one account to another. @@ -72,7 +72,8 @@ The details for each script is provided in the next Usage section. | 5. | Migrate Dashboards | migrate_dashboards.py :arrow_right: migratetags.py | | 6. | Update Monitors | updatemonitors.py | | 7. | Delete Monitors | deletemonitors.py | -| 8. | Migrate Tags | migratetags.py +| 8. | Migrate Tags | migratetags.py | +| 9. | Update Workload Golden Signals | wlgoldensignals.py | The following entities and configurations can be migrated: @@ -490,6 +491,40 @@ output : output/.csv file for each entityName with names of metrics received from that entity +#### 19) python3 wlgoldensignals.py +Automated script for overriding and resetting golden signals for workloads. +####Note: By default workloads only display 4 golden signals. + +usage: wlgoldensignals.py --targetAccount TARGETACCOUNT --targetApiKey TARGETAPIKEY [--targetRegion TARGETREGION] + [--tagName TAGNAME] [--tagValue TAGVALUE] [--goldenSignalsJson GOLDENSIGNALSJSON] + [--resetGoldenSignals] [--domain DOMAIN] [--type TYPE] + +Parameter | Note +-------------- | -------------------------------------------------- +targetAccount | Account containing the workloads +targetRegion | Optional region us (default) or eu +targetApiKey | User API Key for targetAccount +tagName | Tag name to use to find matching workloads +tagValue | Tag value to use to find matching workloads +goldenSignalsJson | File stored under ./goldensignals directory that contains list of metrics in JSON format. [./goldensignals/linuxgoldensignals.json](goldensignals/linuxgoldensignals.json) +resetGoldenSignals | Pass this flag to reset the override golden signals for a domain/type combination +domain | domain for which to reset the golden signals APM , BROWSER , INFRA , MOBILE , SYNTH , EXT +type | type of entity APPLICATION , DASHBOARD , HOST , MONITOR , WORKLOAD + +#### example 1: override golden signals +python3 wlgoldensignals.py --targetAccount ACCT_ID --targetApiKey USER_API_KEY --goldenSignalsJson windowsgoldensignals.json --tagName Environment --tagValue WindowsProduction +The above will find workloads having tag Environment=WindowsProduction and then for each workload +override the golden signals as specified in goldensignals/windowsgoldensignals.json for entities of domain INFRA and type HOST as specified in the json file + +#### example 2: reset override golden signals +python3 wlgoldensignals.py --targetAccount ACCT_ID --targetApiKey USER_API_KEY --resetGoldenSignals --tagName Environment --tagValue WindowsProduction --domain INFRA --type HOST +The above will find workloads having tag Environment=WindowsProduction and then for each workload +reset the golden signals for domain INFRA and type HOST + + + + + ### Logging diff --git a/fetchentities.py b/fetchentities.py index 102667c..a5e78d8 100644 --- a/fetchentities.py +++ b/fetchentities.py @@ -27,6 +27,8 @@ def configure_parser(): parser.add_argument('--infraint', dest='infraint', required=False, action='store_true', help='Pass --infraint to list matching Infrastructure integration entities') parser.add_argument('--mobile', dest='mobile', required=False, action='store_true', help='Pass --mobile to list matching Mobile application entities') parser.add_argument('--lambda', dest='lambda_function', required=False, action='store_true', help='Pass --lambda to list matching Lambda function entities') + parser.add_argument('--workload', dest='workload', required=False, action='store_true', + help='Pass --workloads to list matching Workload entities') parser.add_argument('--tagName', nargs=1, required=False, help='(Optional) Tag name to use when filtering results. Required if --tagValue is passed.') parser.add_argument('--tagValue', nargs=1, required=False, help='(Optional) Tag value to use when filtering results. Required if --tagName is passed.') return parser @@ -63,6 +65,8 @@ def parse_entity_types(args): entity_types.append(ec.MOBILE_APP) if args.lambda_function: entity_types.append(ec.INFRA_LAMBDA) + if args.workload: + entity_types.append(ec.WORKLOAD) return entity_types diff --git a/goldensignals/linuxgoldensignals.json b/goldensignals/linuxgoldensignals.json new file mode 100644 index 0000000..2ef2bde --- /dev/null +++ b/goldensignals/linuxgoldensignals.json @@ -0,0 +1,51 @@ +{ + "domain": "INFRA", + "type": "HOST", + "metrics": [ + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "SystemSample", + "name": "cpuUsage", + "select": "average(cpuPercent)", + "title": "CPU usage (%)", + "where": "operatingSystem = 'linux'" + }, + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "SystemSample", + "select": "average(memoryUsedPercent)", + "name": "memoryUsage", + "title": "Memory usage (%)", + "where": "operatingSystem = 'linux'" + }, + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "SystemSample", + "select": "average(loadAverageFiveMinute)", + "name": "loadAverageFiveMinute", + "title": "Load Average 5 Minutes", + "where": "operatingSystem = 'linux'" + }, + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "StorageSample", + "select": "average(diskUsedPercent)", + "name": "rootDiskUsedPercentage", + "title": "Root Disk Used (%)", + "where": "operatingSystem = 'linux' AND mountPoint = '/'" + }, + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "SystemSample", + "select": "average(swapUsedBytes/swapTotalBytes*100)", + "name": "swapUsedPercentage", + "title": "Swap Used (%)", + "where": "operatingSystem = 'linux'" + } + ] +} \ No newline at end of file diff --git a/goldensignals/windowsgoldensignals.json b/goldensignals/windowsgoldensignals.json new file mode 100644 index 0000000..6397b93 --- /dev/null +++ b/goldensignals/windowsgoldensignals.json @@ -0,0 +1,51 @@ +{ + "domain": "INFRA", + "type": "HOST", + "metrics": [ + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "SystemSample", + "name": "cpuUsage", + "select": "average(cpuPercent)", + "title": "CPU usage (%)", + "where": "operatingSystem = 'windows'" + }, + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "SystemSample", + "select": "average(memoryUsedPercent)", + "name": "memoryUsage", + "title": "Memory usage (%)", + "where": "operatingSystem = 'windows'" + }, + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "SystemSample", + "select": "average(loadAverageFiveMinute)", + "name": "loadAverageFiveMinute", + "title": "Load Average 5 Minutes", + "where": "operatingSystem = 'windows'" + }, + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "StorageSample", + "select": "average(diskUsedPercent)", + "name": "rootDiskUsedPercentage", + "title": "C: Drive Used (%)", + "where": "operatingSystem = 'windows' AND mountPoint= 'C:'" + }, + { + "eventId": "entityGuid", + "facet": "entityName", + "from": "SystemSample", + "select": "average(swapUsedBytes/swapTotalBytes*100)", + "name": "swapUsedPercentage", + "title": "Swap Used (%)", + "where": "operatingSystem = 'windows'" + } + ] +} \ No newline at end of file diff --git a/library/clients/entityclient.py b/library/clients/entityclient.py index 3ce76ad..7c9dc06 100644 --- a/library/clients/entityclient.py +++ b/library/clients/entityclient.py @@ -17,6 +17,7 @@ INFRA_HOST = 'INFRA_HOST' INFRA_INT = 'INFRA_INT' INFRA_LAMBDA = 'INFRA_LAMBDA' +WORKLOAD = 'WORKLOAD' # Mapping to entityType tag values ent_type_lookup = {} @@ -160,6 +161,19 @@ def entity_outline(entity_type): values } } ''' + if entity_type == WORKLOAD: + return ''' ... on WorkloadEntityOutline { + guid + name + type + permalink + entityType + accountId + tags { + key + values + } + }''' def search_query_payload(entity_type, entity_name, acct_id = None): @@ -339,6 +353,8 @@ def get_entities_payload(entity_type, acct_id = None, nextCursor = None, tag_nam gql_search_type = 'HOST' elif entity_type == INFRA_LAMBDA: gql_search_type = 'AWSLAMBDAFUNCTION' + elif entity_type == WORKLOAD: + gql_search_type = 'WORKLOAD' entity_search_query = '''query($matchingCondition: String!) { actor { @@ -381,7 +397,6 @@ def gql_get_entities_by_type(api_key, entity_type, acct_id=None, tag_name=None, while not done: payload = get_entities_payload(entity_type, acct_id, nextCursor, tag_name, tag_value) - response = requests.post(Endpoints.of(region).GRAPHQL_URL, headers=gql_headers(api_key), data=json.dumps(payload)) if response.status_code != 200: done = True @@ -637,6 +652,7 @@ def tags_diff(src_tags, tgt_tags): tags_arr.append(src_tag) return tags_arr + def mutate_tags_payload(entity_guid, arr_tags, mutate_action): apply_tags_query = '''mutation($entityGuid: EntityGuid!, $tags: [TaggingTagInput!]!) {''' + mutate_action + '''(guid: $entityGuid, tags: $tags) { diff --git a/library/clients/goldensignals.py b/library/clients/goldensignals.py new file mode 100644 index 0000000..fb6ea91 --- /dev/null +++ b/library/clients/goldensignals.py @@ -0,0 +1,54 @@ +import os +import json +import library.migrationlogger as logger +import library.clients.gql as nerdgraph +from library.clients.endpoints import Endpoints + +logger = logger.get_logger(os.path.basename(__file__)) + + +class GoldenSignals: + + def __init__(self, region=Endpoints.REGION_US): + self.region = region + pass + + def reset(self, user_api_key, workload_guid, domain, type): + payload = GoldenSignals._reset_golden_signals_payload(workload_guid, domain, type) + logger.debug(json.dumps(payload)) + return nerdgraph.GraphQl.post(user_api_key, payload, self.region) + + + def override(self, user_api_key, workload_guid, domain, type, metrics): + payload = GoldenSignals._override_golden_signals_payload(workload_guid, domain, type, metrics) + logger.debug(json.dumps(payload)) + return nerdgraph.GraphQl.post(user_api_key, payload, self.region) + + @staticmethod + def _reset_golden_signals_payload(workload_guid, domain, type): + mutation_query = '''mutation($context: EntityGoldenContextInput!, $domainType: DomainTypeInput!) { + entityGoldenMetricsReset(context: $context, domainType: $domainType) { + errors {message type} + } + }''' + return {'query': mutation_query, 'variables': {'context': {'guid': workload_guid}, + 'domainType': {'domain': domain, 'type': type} + } + } + + @staticmethod + def _override_golden_signals_payload(workload_guid, domain, type, metrics): + + mutation_query = '''mutation($context: EntityGoldenContextInput!, $domainType: DomainTypeInput!, + $metrics: [EntityGoldenMetricInput!]!) { + entityGoldenMetricsOverride(context: $context, domainType: $domainType, metrics: $metrics) { + errors { message type } + metrics { metrics { name query title } } + } + } + ''' + return {'query': mutation_query, 'variables': {'context': {'guid': workload_guid}, + 'domainType': {'domain': domain, 'type': type}, + 'metrics': metrics + } + } diff --git a/library/clients/gql.py b/library/clients/gql.py new file mode 100644 index 0000000..ca7b4a0 --- /dev/null +++ b/library/clients/gql.py @@ -0,0 +1,34 @@ +import os +import requests +import json +import library.migrationlogger as logger +from library.clients.endpoints import Endpoints + + +logger = logger.get_logger(os.path.basename(__file__)) + + +class GraphQl: + + def __init__(self): + pass + + @staticmethod + def post(per_api_key, payload, region=Endpoints.REGION_US): + result = {} + response = requests.post(Endpoints.of(region).GRAPHQL_URL, headers=GraphQl.headers(per_api_key), + data=json.dumps(payload)) + result['status'] = response.status_code + if response.text: + response_json = response.json() + if 'errors' in response_json: + logger.error('Error : ' + response.text) + result['error'] = response_json['errors'] + else: + logger.debug('Success : ' + response.text) + result['response'] = response_json + return result + + @staticmethod + def headers(api_key): + return {'api-key': api_key, 'Content-Type': 'application/json'} \ No newline at end of file diff --git a/library/localstore.py b/library/localstore.py index 9d3230b..3979baa 100644 --- a/library/localstore.py +++ b/library/localstore.py @@ -166,6 +166,19 @@ def load_json_file(account_id, dir_name, json_file_name): return file_json +def load_json_from_file(dir_name, json_file_name): + # Same as load_json_file without the hard coded DB_DIR/account_id prefixing + file_json = {} + json_dir = Path(dir_name) + if json_dir.exists(): + json_file = json_dir / json_file_name + if json_file.exists(): + file_json = json.loads(json_file.read_text()) + else: + logger.error(dir_name + " does not exist.") + return file_json + + def load_monitor_labels(account_id): return load_json_file(account_id, LABELS_DIR, MONITOR_LABELS_FILE) @@ -195,8 +208,8 @@ def create_output_file(file_name): logger.debug("Creating output file") output_dir = Path("output") output_dir.mkdir(mode=0o777, exist_ok=True) - monitor_names_file = output_dir / file_name - return create_file(monitor_names_file) + output_file = output_dir / file_name + return create_file(output_file) def sanitize(name): diff --git a/library/utils.py b/library/utils.py index ce6cbcf..f44f5bc 100644 --- a/library/utils.py +++ b/library/utils.py @@ -158,6 +158,7 @@ def error_message_and_exit(msg): logger.error(msg) sys.exit() + def get_entity_type(app_condition): if app_condition['type'] in ['apm_app_metric', 'apm_jvm_metric']: return ec.APM_APP diff --git a/wlgoldensignals.py b/wlgoldensignals.py new file mode 100644 index 0000000..146396a --- /dev/null +++ b/wlgoldensignals.py @@ -0,0 +1,109 @@ +import argparse +import os +import json +import library.utils as utils +import library.migrationlogger as logger +import library.clients.entityclient as ec +import library.clients.goldensignals as goldensignals +import library.localstore as store + +logger = logger.get_logger(os.path.basename(__file__)) + + +def configure_parser(): + parser = argparse.ArgumentParser(description='Workload Golden Signal Mutations') + parser.add_argument('--targetAccount', nargs=1, type=int, required=True, help='Target accountId') + parser.add_argument('--targetApiKey', nargs=1, type=str, required=True, help='Target API Key, \ + or set environment variable ENV_TARGET_API_KEY') + parser.add_argument('--targetRegion', type=str, nargs=1, required=False, help='targetRegion us(default) or eu') + parser.add_argument('--tagName', nargs=1, required=False, help='Tag name to lookup workloads.') + parser.add_argument('--tagValue', nargs=1, required=False, help='Tag value to lookup workloads.') + parser.add_argument('--goldenSignalsJson', nargs=1, required=False, help='JSON defining golden signal metrics ' + 'stored in ./goldensignals') + parser.add_argument('--resetGoldenSignals', dest='resetGoldenSignals', required=False, action='store_true', + help='Reset golden signals.') + parser.add_argument('--domain', nargs=1, required=False, help='Needed for resetGoldenSignals. Domain for context ' + 'e.g. APM | BROWSER | INFRA | MOBILE | SYNTH | EXT') + parser.add_argument('--type', nargs=1, required=False, help='Needed for resetGoldenSignals. type for context e.g. ' + 'APPLICATION | DASHBOARD | HOST | MONITOR | WORKLOAD') + + return parser + + +def print_args(args, target_api_key, target_region): + logger.info("Using targetAccount : " + str(args.targetAccount[0])) + logger.info("Using targetApiKey : " + len(target_api_key[:-4])*"*"+target_api_key[-4:]) + logger.info("target region : " + target_region) + if args.goldenSignalsJson: + logger.info("Will apply Golden Signal Override using metrics in " + args.goldenSignalsJson[0]) + if args.resetGoldenSignals: + logger.info("Will reset Golden Signals") + if args.tagName and args.tagValue: + logger.info("For Workloads with " + args.tagName[0] + "=" + args.tagValue[0]) + if args.domain: + logger.info("Using domain " + args.domain[0]) + if args.type: + logger.info("Using type " + args.domain[0]) + + +def override_golden_signals(target_account, target_api_key, wl_tag_name, wl_tag_value, golden_signals_json, + target_region): + logger.info("Applying golden signal overrides") + logger.info("Loading golden signal metrics from " + golden_signals_json) + wl_metrics = store.load_json_from_file("goldensignals", golden_signals_json) + if 'metrics' not in wl_metrics: + utils.error_message_and_exit("Could not load metrics from " + golden_signals_json) + logger.info("Fetching workloads matching " + wl_tag_name + "=" + wl_tag_value) + result = ec.gql_get_entities_by_type(target_api_key, ec.WORKLOAD, target_account, + wl_tag_name, wl_tag_value, target_region) + if 'errors' in result: + utils.error_message_and_exit("Error Fetching workloads matching " + wl_tag_name + "=" + wl_tag_value + " : " + + json.dumps(result['errors'])) + if 'entities' in result and len(result['entities']) == 0: + utils.error_message_and_exit("No workloads matching " + wl_tag_name + "=" + wl_tag_value) + goldenSignals = goldensignals.GoldenSignals(target_region) + for workload in result['entities']: + logger.info('Overriding ' + workload['name'] + ':' + workload['guid']) + goldenSignals.override(target_api_key, workload['guid'], wl_metrics['domain'], wl_metrics['type'], + wl_metrics['metrics']) + + +def reset_golden_signals(target_account, target_api_key, wl_tag_name, wl_tag_value, target_region): + logger.info("Resetting golden signals") + result = ec.gql_get_entities_by_type(target_api_key, ec.WORKLOAD, target_account, wl_tag_name, wl_tag_value, + target_region) + goldenSignals = goldensignals.GoldenSignals(target_region) + for workload in result['entities']: + logger.info('Resetting ' + workload['name'] + ':' + workload['guid']) + result = goldenSignals.reset(target_api_key, workload['guid'], 'INFRA', 'HOST' ) + logger.info(json.dumps(result)) + + +def main(): + parser = configure_parser() + args = parser.parse_args() + target_api_key = utils.ensure_target_api_key(args) + if not target_api_key: + utils.error_and_exit('target api key', 'ENV_TARGET_API_KEY') + target_region = utils.ensure_target_region(args) + if not args.goldenSignalsJson and not args.resetGoldenSignals: + utils.error_message_and_exit("Either --goldenSignalsJson or --resetGoldenSignals must be passed") + if args.goldenSignalsJson and args.resetGoldenSignals: + utils.error_message_and_exit("Only one of --goldenSignalsJson or --resetGoldenSignals must be passed") + if args.goldenSignalsJson and not (args.tagName and args.tagValue): + utils.error_message_and_exit("tagName and tagValue are required to look up workloads " + "and apply goldenSignalsJson") + if args.resetGoldenSignals and not (args.tagName and args.tagValue): + utils.error_message_and_exit("tagName and tagValue are required to look up workloads to resetGoldenSignals") + if args.resetGoldenSignals and not (args.domain or args.type): + utils.error_message_and_exit("domain and type are required to set context for override signals") + print_args(args, target_api_key, target_region) + if args.goldenSignalsJson: + override_golden_signals(args.targetAccount[0], target_api_key, args.tagName[0], args.tagValue[0], + args.goldenSignalsJson[0], target_region) + elif args.resetGoldenSignals: + reset_golden_signals(args.targetAccount[0], target_api_key, args.tagName[0], args.tagValue[0], target_region) + + +if __name__ == '__main__': + main()