From 4bdab1a17f6e0a38d5fd074a6690b2821abe2e78 Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Mon, 19 Feb 2024 14:31:11 +0530 Subject: [PATCH 01/12] feat: push data to stackdriver --- tools/target-server-validator/README.md | 43 +++++++------ .../target-server-validator/input.properties | 7 ++- tools/target-server-validator/main.py | 12 ++++ .../target-server-validator/requirements.txt | 1 + tools/target-server-validator/utilities.py | 61 +++++++++++++++++++ 5 files changed, 104 insertions(+), 20 deletions(-) diff --git a/tools/target-server-validator/README.md b/tools/target-server-validator/README.md index 41078b12..eeb0fac5 100644 --- a/tools/target-server-validator/README.md +++ b/tools/target-server-validator/README.md @@ -25,30 +25,35 @@ bash callout/build_java_callout.sh ``` [source] -baseurl=https://x.x.x.x/v1 # Apigee Base URL. e.g http://management-api.apigee-opdk.corp:8080 -org=xxx-xxxx-xxx-xxxxx # Apigee Org ID -auth_type=basic # API Auth type basic | oauth +baseurl=https://x.x.x.x/v1 # Apigee Base URL. e.g http://management-api.apigee-opdk.corp:8080 +org=xxx-xxxx-xxx-xxxxx # Apigee Org ID +auth_type=basic # API Auth type basic | oauth [target] -baseurl=https://apigee.googleapis.com/v1 # Apigee Base URL -org=xxx-xxxx-xxx-xxxxx # Apigee Org ID -auth_type=oauth # API Auth type basic | oauth +baseurl=https://apigee.googleapis.com/v1 # Apigee Base URL +org=xxx-xxxx-xxx-xxxxx # Apigee Org ID +auth_type=oauth # API Auth type basic | oauth [csv] -file=input.csv # Path to input CSV. Note: CSV needs HOST & PORT columns -default_port=443 # default port if port is not provided in CSV +file=input.csv # Path to input CSV. Note: CSV needs HOST & PORT columns +default_port=443 # default port if port is not provided in CSV [validation] -check_csv=true # 'true' to validate Targets in input csv -check_proxies=true # 'true' to validate Proxy Targets else 'false' -skip_proxy_list=mock1,stream # Comma sperated list of proxies to skip validation; -proxy_export_dir=export # Export directory needed when check_proxies='true' -api_env=dev # Target Environment to deploy Validation API Proxy -api_name=target_server_validator # Target API Name of Validation API Proxy -api_force_redeploy=false # set 'true' to Re-deploy Target API Proxy -api_hostname=example.apigee.com # Target VirtualHost or EnvGroup Domain Name -api_ip= # IP address corresponding to api_hostname. Use if DNS record doesnt exist -report_format=csv # Report Format. Choose csv or md (defaults to md) +check_csv=true # 'true' to validate Targets in input csv +check_proxies=true # 'true' to validate Proxy Targets else 'false' +skip_proxy_list=mock1,stream # Comma sperated list of proxies to skip validation; +proxy_export_dir=export # Export directory needed when check_proxies='true' +api_env=dev # Target Environment to deploy Validation API Proxy +api_name=target_server_validator # Target API Name of Validation API Proxy +api_force_redeploy=false # set 'true' to Re-deploy Target API Proxy +api_hostname=example.apigee.com # Target VirtualHost or EnvGroup Domain Name +api_ip= # IP address corresponding to api_hostname. Use if DNS record doesnt exist +report_format=csv # Report Format. Choose csv or md (defaults to md) + +[cloud_monitoring] +stack_driver=true # set 'true' to push target server's host and status to stack driver +project_id=xxx-xxx-xx # Project id of GCP project where the data will be pushed +metric_name=custom.googleapis.com/ # Replace with custom metric name ``` * Sample input CSV with target servers @@ -112,7 +117,7 @@ The response will look like this - { "host": "example2.com", "port": 443, - "status" : "UNKNOWN_HOST" + "status" : "UNKNOWN_HOST" }, // and so on ] diff --git a/tools/target-server-validator/input.properties b/tools/target-server-validator/input.properties index e15ba143..075e4f1a 100644 --- a/tools/target-server-validator/input.properties +++ b/tools/target-server-validator/input.properties @@ -23,4 +23,9 @@ api_force_redeploy=true api_hostname=example.apigee.com api_ip= report_format=md -allow_insecure=false \ No newline at end of file +allow_insecure=false + +[stack_driver] +stack_driver=true +project_id=xx-xxx-xxx +metric_name=custom.googleapis.com/ diff --git a/tools/target-server-validator/main.py b/tools/target-server-validator/main.py index ccc6eb47..84931cb2 100644 --- a/tools/target-server-validator/main.py +++ b/tools/target-server-validator/main.py @@ -18,6 +18,7 @@ import os import sys import json +import time from utilities import ( # pylint: disable=import-error parse_config, create_proxy_bundle, @@ -29,6 +30,8 @@ has_templating, get_row_host_port, run_parallel, + create_custom_metric, + report_metric, ) from apigee_utils import Apigee # pylint: disable=import-error from base_logger import logger @@ -40,6 +43,7 @@ def main(): check_proxies = cfg["validation"].getboolean("check_proxies") proxy_export_dir = cfg["validation"]["proxy_export_dir"] report_format = cfg["validation"]["report_format"] + stack_driver = cfg["stack_driver"]["stack_driver"] allow_insecure = cfg["validation"].getboolean("allow_insecure") if report_format not in ["csv", "md"]: report_format = "md" @@ -250,6 +254,14 @@ def main(): logger.info(f"Dumping report to file {report_file}") write_md_report(report_file, final_report) + if stack_driver: + project_id = cfg["stack_driver"]["project_id"] + metric_name = cfg["stack_driver"]["metric_name"] + logger.info("Dumping data to stack driver") + descriptor = create_custom_metric(project_id, metric_name) + time.sleep(5) + report_metric(project_id, descriptor, final_report) + if __name__ == "__main__": main() diff --git a/tools/target-server-validator/requirements.txt b/tools/target-server-validator/requirements.txt index fca06aa9..76c3639b 100644 --- a/tools/target-server-validator/requirements.txt +++ b/tools/target-server-validator/requirements.txt @@ -18,3 +18,4 @@ xmltodict==0.13.0 requests==2.31.0 forcediphttpsadapter==1.0.2 +google-cloud-monitoring==2.19.1 diff --git a/tools/target-server-validator/utilities.py b/tools/target-server-validator/utilities.py index f0bf56bc..c00a6a45 100644 --- a/tools/target-server-validator/utilities.py +++ b/tools/target-server-validator/utilities.py @@ -21,6 +21,10 @@ import zipfile import csv from urllib.parse import urlparse +import time +from google.api import label_pb2 as ga_label +from google.cloud import monitoring_v3 +from google.api import metric_pb2 as ga_metric import requests import xmltodict import urllib3 @@ -268,3 +272,60 @@ def run_parallel(func, args, workers=10): logger.info("No exception information available.") logger.error(f"{future} generated an exception") return data + + +def create_custom_metric(project_id, metric_name): + client = monitoring_v3.MetricServiceClient() + project_name = f"projects/{project_id}" + + # Create metric descriptor + descriptor = ga_metric.MetricDescriptor() + descriptor.type = metric_name + descriptor.metric_kind = ga_metric.MetricDescriptor.MetricKind.GAUGE + descriptor.value_type = ga_metric.MetricDescriptor.ValueType.BOOL + descriptor.labels.extend([ + ga_label.LabelDescriptor(key='hostname', value_type='STRING'), + ga_label.LabelDescriptor(key='status', value_type='STRING') + ]) + try: + descriptor = client.create_metric_descriptor(name=project_name, metric_descriptor=descriptor) # noqa + return descriptor + except Exception as e: + logger.error(f"Error while creating the metric descriptor. ERROR-INFO: {e}") # noqa + return None + + +def report_metric(project_id, metric_descriptor, sample_data): + client = monitoring_v3.MetricServiceClient() + project_name = f"projects/{project_id}" + + series = monitoring_v3.TimeSeries() + + # Check if metric descriptor exists + if not metric_descriptor: + logger.error("Error while pushing the data to stackdriver. ERROR-INFO: Metric descriptor does not exist.") # noqa + return + + series.metric.type = metric_descriptor.type + series.resource.type = 'global' + + now = time.time() + seconds = int(now) + nanos = int((now - seconds) * 10 ** 9) + interval = monitoring_v3.TimeInterval({'end_time': {'seconds': seconds, 'nanos': nanos}}) # noqa + + try: + for data in sample_data: + status = True if data[5] == 'REACHABLE' else False + + point = monitoring_v3.Point({'interval': interval, 'value': {'bool_value': status}}) # noqa + series.metric.labels['hostname'] = data[2] + series.metric.labels['status'] = data[5] + series.points = [point] + + client.create_time_series(name=project_name, time_series=[series]) + logger.debug(f"Pushed to stackdriver - {data[2]} {data[5]}") + + logger.info("Successfully pushed data to stackdriver") + except Exception as e: + logger.error(f"Error while pushing the data to stackdriver. ERROR-INFO: {e}") # noqa From 77ccf13f8e5fa2f3681c65ed8844411f11f7ab4f Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Mon, 26 Feb 2024 22:34:46 +0530 Subject: [PATCH 02/12] feat: enable monitoring dashboard and alerting policy --- tools/target-server-validator/README.md | 4 +- .../target-server-validator/input.properties | 12 ++- tools/target-server-validator/main.py | 37 ++++---- .../target-server-validator/requirements.txt | 1 + tools/target-server-validator/utilities.py | 89 +++++++++++++++++-- 5 files changed, 117 insertions(+), 26 deletions(-) diff --git a/tools/target-server-validator/README.md b/tools/target-server-validator/README.md index eeb0fac5..a73407da 100644 --- a/tools/target-server-validator/README.md +++ b/tools/target-server-validator/README.md @@ -50,8 +50,8 @@ api_hostname=example.apigee.com # Target VirtualHost or EnvGro api_ip= # IP address corresponding to api_hostname. Use if DNS record doesnt exist report_format=csv # Report Format. Choose csv or md (defaults to md) -[cloud_monitoring] -stack_driver=true # set 'true' to push target server's host and status to stack driver +[gcp_metrics] +enable_gcp_metrics=true # set 'true' to push target server's host and status to stack driver project_id=xxx-xxx-xx # Project id of GCP project where the data will be pushed metric_name=custom.googleapis.com/ # Replace with custom metric name ``` diff --git a/tools/target-server-validator/input.properties b/tools/target-server-validator/input.properties index 075e4f1a..feb069ca 100644 --- a/tools/target-server-validator/input.properties +++ b/tools/target-server-validator/input.properties @@ -25,7 +25,11 @@ api_ip= report_format=md allow_insecure=false -[stack_driver] -stack_driver=true -project_id=xx-xxx-xxx -metric_name=custom.googleapis.com/ +[gcp_metrics] +enable_gcp_metrics=true +project_id=xxx-xxx-xxx +metric_name=custom.googleapis.com/host_status_v10 +enable_dashboard=true +dashboard_title=Apigee Target Server Health Monitoring Dashboard v3 +alert_policy_name=Apigee Target Server Validator Policy v3 +notification_channel_id=xxxx \ No newline at end of file diff --git a/tools/target-server-validator/main.py b/tools/target-server-validator/main.py index 84931cb2..261aed8c 100644 --- a/tools/target-server-validator/main.py +++ b/tools/target-server-validator/main.py @@ -32,6 +32,7 @@ run_parallel, create_custom_metric, report_metric, + create_custom_dashboard, ) from apigee_utils import Apigee # pylint: disable=import-error from base_logger import logger @@ -42,8 +43,8 @@ def main(): cfg = parse_config("input.properties") check_proxies = cfg["validation"].getboolean("check_proxies") proxy_export_dir = cfg["validation"]["proxy_export_dir"] + enable_gcp_metrics = cfg["gcp_metrics"].getboolean("enable_gcp_metrics") report_format = cfg["validation"]["report_format"] - stack_driver = cfg["stack_driver"]["stack_driver"] allow_insecure = cfg["validation"].getboolean("allow_insecure") if report_format not in ["csv", "md"]: report_format = "md" @@ -242,26 +243,32 @@ def main(): else: logger.error(output.get("error", "Unknown Error occured while calling proxy")) # noqa + if enable_gcp_metrics: + project_id = cfg["gcp_metrics"]["project_id"] + metric_name = cfg["gcp_metrics"]["metric_name"] + enable_dashboard = cfg["gcp_metrics"]["enable_dashboard"] + logger.info("Dumping data to stack driver") + descriptor = create_custom_metric(project_id, metric_name) + report_metric(project_id, descriptor, final_report) + if enable_dashboard: + logger.info(f"Creating dashboard in project {project_id}") + dashboard_title=cfg["gcp_metrics"]["dashboard_title"] + alert_policy_name=cfg["gcp_metrics"]["alert_policy_name"] + notification_channel_id = cfg["gcp_metrics"]["notification_channel_id"] + create_custom_dashboard(project_id, dashboard_title, metric_name, alert_policy_name, notification_channel_id) + + elif report_format == "md": + report_file = "report.md" + logger.info(f"Dumping report to file {report_file}") + write_md_report(report_file, final_report) + # Write CSV Report # TODO: support relative report path - if report_format == "csv": + elif report_format == "csv": report_file = "report.csv" logger.info(f"Dumping report to file {report_file}") write_csv_report(report_file, final_report) - if report_format == "md": - report_file = "report.md" - logger.info(f"Dumping report to file {report_file}") - write_md_report(report_file, final_report) - - if stack_driver: - project_id = cfg["stack_driver"]["project_id"] - metric_name = cfg["stack_driver"]["metric_name"] - logger.info("Dumping data to stack driver") - descriptor = create_custom_metric(project_id, metric_name) - time.sleep(5) - report_metric(project_id, descriptor, final_report) - if __name__ == "__main__": main() diff --git a/tools/target-server-validator/requirements.txt b/tools/target-server-validator/requirements.txt index 76c3639b..4143da31 100644 --- a/tools/target-server-validator/requirements.txt +++ b/tools/target-server-validator/requirements.txt @@ -19,3 +19,4 @@ xmltodict==0.13.0 requests==2.31.0 forcediphttpsadapter==1.0.2 google-cloud-monitoring==2.19.1 +google-cloud-monitoring-dashboards==2.14.1 diff --git a/tools/target-server-validator/utilities.py b/tools/target-server-validator/utilities.py index c00a6a45..17238ec3 100644 --- a/tools/target-server-validator/utilities.py +++ b/tools/target-server-validator/utilities.py @@ -25,6 +25,9 @@ from google.api import label_pb2 as ga_label from google.cloud import monitoring_v3 from google.api import metric_pb2 as ga_metric +from google.cloud import monitoring_dashboard_v1 +from google.cloud import monitoring_v3 +from google.protobuf import duration_pb2 import requests import xmltodict import urllib3 @@ -282,7 +285,7 @@ def create_custom_metric(project_id, metric_name): descriptor = ga_metric.MetricDescriptor() descriptor.type = metric_name descriptor.metric_kind = ga_metric.MetricDescriptor.MetricKind.GAUGE - descriptor.value_type = ga_metric.MetricDescriptor.ValueType.BOOL + descriptor.value_type = ga_metric.MetricDescriptor.ValueType.DOUBLE descriptor.labels.extend([ ga_label.LabelDescriptor(key='hostname', value_type='STRING'), ga_label.LabelDescriptor(key='status', value_type='STRING') @@ -294,6 +297,13 @@ def create_custom_metric(project_id, metric_name): logger.error(f"Error while creating the metric descriptor. ERROR-INFO: {e}") # noqa return None +def get_status_int(status): + if status == "REACHABLE": + return 1 + elif status == "NOT_REACHABLE": + return 0.5 + elif status == "UNKNOWN_HOST": + return 0 def report_metric(project_id, metric_descriptor, sample_data): client = monitoring_v3.MetricServiceClient() @@ -316,16 +326,85 @@ def report_metric(project_id, metric_descriptor, sample_data): try: for data in sample_data: - status = True if data[5] == 'REACHABLE' else False - - point = monitoring_v3.Point({'interval': interval, 'value': {'bool_value': status}}) # noqa + point = monitoring_v3.Point({'interval': interval, 'value': {'double_value': get_status_int(data[5])}}) # noqa series.metric.labels['hostname'] = data[2] series.metric.labels['status'] = data[5] series.points = [point] client.create_time_series(name=project_name, time_series=[series]) logger.debug(f"Pushed to stackdriver - {data[2]} {data[5]}") - logger.info("Successfully pushed data to stackdriver") except Exception as e: logger.error(f"Error while pushing the data to stackdriver. ERROR-INFO: {e}") # noqa + + +def create_alert_policy(project_name, policy_name, metric_name, notification_channel_id): + client = monitoring_v3.AlertPolicyServiceClient() + conditions = [ + monitoring_v3.AlertPolicy.Condition( + display_name="Target Server Validator Policy", + condition_threshold=monitoring_v3.AlertPolicy.Condition.MetricThreshold( + filter=f"resource.type = \"global\" AND metric.type = \"{metric_name}\"", + comparison=monitoring_v3.ComparisonType.COMPARISON_LT, + threshold_value=0.75, + duration=duration_pb2.Duration(seconds=0), + aggregations=[ + monitoring_v3.Aggregation( + alignment_period=duration_pb2.Duration(seconds=300), + per_series_aligner=monitoring_v3.Aggregation.Aligner.ALIGN_NEXT_OLDER, + cross_series_reducer=monitoring_v3.Aggregation.Reducer.REDUCE_NONE, + ) + ], + trigger=monitoring_v3.AlertPolicy.Condition.Trigger( + count=1, + ) + ), + ) + ] + + notification_channels = [f"projects/apigee-hybrid-testing-415007/notificationChannels/{notification_channel_id}"] + policy = monitoring_v3.AlertPolicy( + display_name=policy_name, + conditions=conditions, + notification_channels=notification_channels, + combiner=monitoring_v3.AlertPolicy.ConditionCombinerType.OR, + ) + + created_policy = client.create_alert_policy( + name=project_name, + alert_policy=policy + ) + logger.info(f"Created alert policy: {created_policy.name}") + return created_policy.name + + +def create_custom_dashboard(project_id, dashboard_title, metric_name, policy_name, notification_channel_id): + client = monitoring_dashboard_v1.DashboardsServiceClient() + request = monitoring_dashboard_v1.ListDashboardsRequest(parent=f"projects/{project_id}") + + existing_dashboards = client.list_dashboards(request=request).dashboards + for dashboard in existing_dashboards: + if dashboard.display_name == dashboard_title: + logger.info(f"Dashboard '{dashboard_title}' already exists. Skipping creation.") + return + + dashboard = monitoring_dashboard_v1.Dashboard() + dashboard.display_name = dashboard_title + grid_layout = monitoring_dashboard_v1.GridLayout( + widgets=[] + ) + dashboard.grid_layout = grid_layout + + #create alerting policy + project_name=f"projects/{project_id}" + alert_policy_name = create_alert_policy(project_name, policy_name, metric_name, notification_channel_id) + widget = monitoring_dashboard_v1.Widget() + widget.alert_chart = monitoring_dashboard_v1.AlertChart(name=alert_policy_name) + dashboard.grid_layout.widgets.append(widget) + + request = monitoring_dashboard_v1.CreateDashboardRequest( + parent=f"projects/{project_id}", + dashboard=dashboard, + ) + response = client.create_dashboard(request=request) + logger.info(f"Dashboard created: {response.name}") \ No newline at end of file From f399e70c1ab4478d7c570b8f151382b00457d4c4 Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Thu, 29 Feb 2024 13:17:32 +0530 Subject: [PATCH 03/12] feat: updated arguments and enabled gcs for outputs --- tools/target-server-validator/README.md | 81 +++- .../images/dashboard.png | Bin 0 -> 180093 bytes .../target-server-validator/input.properties | 15 +- tools/target-server-validator/main.py | 437 ++++++++++-------- .../target-server-validator/requirements.txt | 1 + tools/target-server-validator/utilities.py | 83 +++- 6 files changed, 368 insertions(+), 249 deletions(-) create mode 100644 tools/target-server-validator/images/dashboard.png diff --git a/tools/target-server-validator/README.md b/tools/target-server-validator/README.md index a73407da..3fcfa948 100644 --- a/tools/target-server-validator/README.md +++ b/tools/target-server-validator/README.md @@ -11,6 +11,7 @@ Validation is done by deploying a sample proxy which check if HOST & PORT is ope * Python3.x * Java * Maven +3.9.6 * Please install the required Python dependencies ``` python3 -m pip install -r requirements.txt @@ -25,35 +26,44 @@ bash callout/build_java_callout.sh ``` [source] -baseurl=https://x.x.x.x/v1 # Apigee Base URL. e.g http://management-api.apigee-opdk.corp:8080 -org=xxx-xxxx-xxx-xxxxx # Apigee Org ID -auth_type=basic # API Auth type basic | oauth +baseurl=https://x.x.x.x/v1 # Apigee Base URL. e.g http://management-api.apigee-opdk.corp:8080 +org=xxx-xxxx-xxx-xxxxx # Apigee Org ID +auth_type=basic # API Auth type basic | oauth [target] -baseurl=https://apigee.googleapis.com/v1 # Apigee Base URL -org=xxx-xxxx-xxx-xxxxx # Apigee Org ID -auth_type=oauth # API Auth type basic | oauth +baseurl=https://apigee.googleapis.com/v1 # Apigee Base URL +org=xxx-xxxx-xxx-xxxxx # Apigee Org ID +auth_type=oauth # API Auth type basic | oauth [csv] -file=input.csv # Path to input CSV. Note: CSV needs HOST & PORT columns -default_port=443 # default port if port is not provided in CSV +file=input.csv # Path to input CSV. Note: CSV needs HOST & PORT columns +default_port=443 # default port if port is not provided in CSV [validation] -check_csv=true # 'true' to validate Targets in input csv -check_proxies=true # 'true' to validate Proxy Targets else 'false' -skip_proxy_list=mock1,stream # Comma sperated list of proxies to skip validation; -proxy_export_dir=export # Export directory needed when check_proxies='true' -api_env=dev # Target Environment to deploy Validation API Proxy -api_name=target_server_validator # Target API Name of Validation API Proxy -api_force_redeploy=false # set 'true' to Re-deploy Target API Proxy -api_hostname=example.apigee.com # Target VirtualHost or EnvGroup Domain Name -api_ip= # IP address corresponding to api_hostname. Use if DNS record doesnt exist -report_format=csv # Report Format. Choose csv or md (defaults to md) +check_csv=true # 'true' to validate Targets in input csv +check_proxies=true # 'true' to validate Proxy Targets else 'false' +skip_proxy_list=mock1,stream # Comma separated list of proxies to skip validation; +proxy_export_dir=export # Export directory needed when check_proxies='true' +api_env=dev # Target Environment to deploy Validation API Proxy +api_name=target_server_validator # Target API Name of Validation API Proxy +api_force_redeploy=false # set 'true' to Re-deploy Target API Proxy +api_hostname=example.apigee.com # Target VirtualHost or EnvGroup Domain Name +api_ip= # IP address corresponding to api_hostname. Use if DNS record doesnt exist +report_format=csv # Report Format. Choose csv or md (defaults to md) [gcp_metrics] -enable_gcp_metrics=true # set 'true' to push target server's host and status to stack driver -project_id=xxx-xxx-xx # Project id of GCP project where the data will be pushed -metric_name=custom.googleapis.com/ # Replace with custom metric name +enable_gcp_metrics=true # set 'true' to push target server's host and status to stack driver +project_id=xxx-xxx-xxx # Project id of GCP project where the data will be pushed +metric_name=custom.googleapis.com/ # Replace with custom metric name +enable_dashboard=true # set 'true' to create the dashboard with alerting policy +dashboard_title=Apigee Target Server Monitoring Dashboard # Monitoring Dashboard Title +alert_policy_name=Apigee Target Server Validator Policy # Alerting Policy Name +notification_channel_id=xxxxxxxx # Notification Channel id + +[gcs_bucket] +bucket_name=target-server-validator-gcs # GCS bucket name for storing the --scan output +bucket_project_id=xx-xxx-xx # GCS bucket project id +file_path_in_bucket=scan_output.txt # path to output file ``` * Sample input CSV with target servers @@ -81,16 +91,32 @@ export APIGEE_ACCESS_TOKEN=$(gcloud auth print-access-token) # Access * Export Proxy Bundle * Parse Each Proxy Bundle for Target * Run Validate API against each Target (optional) -* Generate csv/md Report +* Generate csv/md Report or push data to GCP Monitoring Dashboard ## Usage -Run the script as below +The script supports the below arguments + +* `--onboard` Toggle to onboard validator proxy, custom metric descriptors and dashboard +* `--scan` Toggle to read all resources +* `--monitor` Toggle to check the status of target servers and push to GCP Logging + +To onboard, run +``` +python3 main.py --onboard +``` + +To scan, run ``` -python3 main.py +python3 main.py --scan ``` -This script deploys an API proxy to validate if the target servers are reachable or not. To use the API proxy, make sure your payloads adhere to the following format: +To monitor, run +``` +python3 main.py --monitor +``` + +--onboard deploys an API proxy to validate if the target servers are reachable or not. To use the API proxy, make sure your payloads adhere to the following format: ```json [ @@ -127,3 +153,8 @@ The response will look like this - Validation Report: `report.md` OR `report.csv` can be found in the same directory as the script. Please check a [Sample report](report.md) + +## GCP Monitoring Dashboard +The script can also create a GCP Monitoring Dashboard with an alerting widget like shown below: + +![GCP Monitoring Dashboard](images/dashboard.png) \ No newline at end of file diff --git a/tools/target-server-validator/images/dashboard.png b/tools/target-server-validator/images/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..62e04dbf79cf504dbfaf8c964e4abeb31d6f3256 GIT binary patch literal 180093 zcmb@t1ymeO*DZ`ou)*Ce2~L8$TaXZ3!{E+fgA;-W4Fn1965JhvySoPlcXzpyJg@!# zUF-dxZ>>9P^>j~nS66k_Is5Fht0q)MNe1Hu$qN`57z{aCNmUpa)E*cZL~#@(=o$La z`U)5rbTLZ_2^BdB2`UvwdoxQLQy3W8&^S$Gtq=VKX}U4dq86D7s4b{8s&KNXnTXK_ z2&B}g0Qqpg44@1Fz8>0ZnvW8iQg{MXu96kyFRxm)R8(fVKkK{4!ZjW-ttKBQ3p_t= z5BZLy2sj;+!%QhTN@m%pk$)mHO57@pG*rL zJ>9|ZR1;S}{MGi;AD?S1PHr0XV4`1!d~k1AA`8KS`=se4wJrz~qPpVcktX}a2vG+} zMAa3R%c~z*l*_9gc|BmW4%7nF!DZxhtc6U%s ze;@GYMFf&s(0~x{nQE^W;mT93S_IMRvZGpzHuU} z7%H2Zg_qM1{E+#~??0-sWBdb=7!(TlKvmmENGB;;{G!6xPQxa#!^}nui3!*z%^do* z#~v1hZb)>%HrsnP*(JvD;yxy2Df1SX%!zP-g1C~NfK5m8RP=XXjL-vlYV5K)ks0WdqFaMrIAUvTu5spH zu9epzoFH(1_I2C-IS_bi*7^GI`T7BHpK2t|pw})NBsei0#2o6^1p`Fk2(btw{Uuya zNP$`AQ_c@TUoy6!SUp5lMokTn3D^%{?Kph(S^QM_eI&fTiiqsjuqjF>KV30J%_Yj~ zpgk4h*>;Ri@JDcu>>$IPdHhOdsileHG2e-&MRXrGk{1G%xWx4PF^_%RB2j8JA6&>a zzHZ(1KJ*zYl8^9M*KeCY7gJV_`rFQp#?Ju{U}l0+J4TFUDx^hwl|CwJ3An!d60A9k zJHJPAMnD_Jh_;9Fv}V84FaPHIzS{QG{)N;^LT@^?w+#G@+W5N)Dk0jakpM#k;Z&9l z{>U5w0lp>}s>jwoLqq#4(^U+~{6~Sh6D*i!WpqqTirbOEG?>g3A4w?gqJV%OMNjqAsuLc`j zXQt_T94^lzvv{&8?MX@aXX&bVfQG&6}Da~<9?^-2~7<}_AREl4% zeB+g8Fon(itg6f>(IDfVU0iIFcaTX|!mN^3rkB~Qd7E-3u^II`>9y-?=D5(;IB}-0 z7uA)1ENQq)-`B0*2tJ0Enry5agR@kGK zT&$6yQR*ss7^9QUue~gvnmh77y~rTfN9!@~?h}%takX@gbYzceL@LQPm&aSVB#k7$ zB*9^tVV0zPF3L&vB)$kK$OOSe?Zn7fV@-(8jE+soyAsusWgT*z$AzQ?)&Fb zalX3qqUw8%cC&WoJVn+07M@d(d(y4dIl;LP8g3Yfpo(RW`A92C`?ED7tw=A2`=?(m zS{6+%k0)hI$Ph-_Q9Lfpk9^CK=x&_U)9hFQUTd;(XxNl@BHwQ8tLeTh zN^VM?D_JR-UG5O-ZdrI_kO;%>)*9g57CSk)PTU4fL9+9c&C|AhM%$Xnnk$=KJubs* zC!w-`Gc9n;o-KHY$G^Wk_c={9qVqA8|}8;v|x`%H3xEPS=dViSRtPEw*j$ zIr`Z%p3xd}-vovLK&FB-hFw5RM5#r5L)FLpQmBBnKxB@~P2;2=ATF!bHxQj{L(83@ zKX`=Yf|ZK9K$3?;K&nA}^>T?-k8DK{lbn#>Zo1wRJ080uq(>IeAE}4$E;7Jxr8D)_ z0_d#ftbQ?>IL=XJya2SU_y7d=w1%BW=~94sRu~L$R%0^a**P+~*SvwEKyBdc&3njp zcw3@LG@Y!KJOe!=oxK7@B7oOuIN&I>6gP}%`$zoNXFx}?v-HjG-8ebk?VcTp#?hQ<=9Q^tiS?Y3gRkG}Z)!Y4U! zbbZ9?yDh(G`$4c%q7(P1)K=bz?RRtGG7E$2hhMh>1S$k3EH^D7XH(NAiQlx~CFvKu{_;Bq?yZ$;HgZ@C({E4#(HiZTSwg>*Z*)N93 z3Arr$T1T~BoiEm3tbHM6^42_hIr)<5rDwC%(vP6&i)s3z)za^kTV4$cw>>rAYGP{i z@z?Mb_nBu(w61eGv(G%@e5cc_I``{qg@@}1e{nC9=j3n#R$Uc%4h*-Fn-vJI)RfrOc30TT@J&WcY*NV*Ln#Zx@(l*LHhkc9s4WGi@%HGYb zk&4vbvKs?F7p`A>^VSE}gk>^%$IaNjkeT67o+i6fyUV4K*2g`ir_@pK_A2cXQ$v|g z+k@Ylf0BsJ^W5a-jLU)1&(15Eil`&uEn%Niy6gUgqpbX?{LRsKY13D{*JOtndo2%M z9p^`cS%m5LU!JFKs_t@!2mO6-AE#GDTHEg9Zl-_j=h|Pa+W79D)tXXS{c3MY0jIAf zc#S=mLuet!i}_v3qr%G1;&-a2(^uB#b>;TgNKQz|Qe+%5S4^MUzvSh@o_GkrOwqwu zoOnrprxJPYe?cQsH2}C#*7A}5hR*}DxAyGmO!?5%fckT#$`bvf@l*PuFRWIHm*v(b^%yj#_+qLh zXQrqK^BQ`L0)qfc0)q%Wf`x9vu%!PwmWF)^1OMkd91KjbB@Dtp?oop7e}AH(+wU@e z?crmBV347|aG;x82HZdIM(xRf|K~BHIP@Bf*arzYIq3d_v7@P}os)&VGu&uD3-ko4 zgRHg_3=9GN?+sQ?mF@)k{5eZCEoUvocl^fowyZ`b_CQlsH(Q6_&w&wiR#nR2xMqAR-7TRafI)pjiyyX`BQ{ex)^zSPFqo~%ui*jL_7v3$4^y_}>HekHY_X@gD^R*?zzKfAqy)9sTDlw5Nq%2(tZa)P!F& zDX#xEJTgm3Wi{v?YG%JbaGB7rmw)Y{$FOX92NHxlSWtG~6QwJgaWK~jU2x#}$9Qpsg zl0S{}8xf}py@zd#RQMsk@%tO|g&^~`BMc~?{YO7!xT6el&Fb0x`~++F>!+XHiu*Q3@7Cs) zo}n&8^ys_iQN!}Dc7J$beOe(HD-UGs80;dyH1}9!ONJxi2rx( zBJewf^ft-GGF&d+>2_oMYdkWp0Yuz3iKP6_79U=Wp<1WhZl|xZ)c^Xd*p8f=v=i@K z+PQwC$n@Vh|M|l9Y$_*N56oUh1Ex==}}Cd8vbM7_T=MB7;Qd zQ7jyjQ=HxjF#0wSrYGpv3f_eiB_OkeS^qr)3lc>|d6&#R+;TaiD>k_^o-ObNPbQZ? z^sS1xQ}T;HMs|k}mnw|xtn*0KEA}KlpcU)9K!WVJlx*Khd_v4+^^Nqcg$gg*nR4vH zMvAc)>#Ua~?3%?0-!^khNiG)xiS#c>lnCY{ZdhyxU?Tv9an_MdG6`RK#B<-GtA0g% z<&5iZxwD^7*e5O3cJIBDwN3%dU?o}%XFYiBz&}V}LG!1Dz{df-gl~mKq04U*tcv&6 zG-{ZOK;mxM0P&Dm_J*Jk1nNi;T#1ZSz$da^O3C3wKz`Y)D!{f!)q`~SzTyRo{Xc!` zzeCJU0!s5~52;;GIB~9)nWYSjz}L59k-WO!|7wd20>3xrPS;)NbbY%&a+4H+c8*Hj zUKdxSRs<>g$4|P><5FmAvu(A4+z3g5FpuB1;nqnX z)o|A@_-?Pc`9?xO!+VH%vwzP!(Eacb4BR*4%6A(!Vid0K%-+jmqalK6EHeO9@qawb z&*v{=(-J8bX?eQa_xn|OvmQpcSCU%a6RbYWdj8X(Q5?}(Q6wob9j%2-;4^9y+ik_mH9aE?Nw&D+y*ao z54Ko(U8AaWxHZ>$eER0_9vwhL+#7g z3d;7^5Z-6bJ^C(QZ#*Db>8pyLfoSSa(8^ms!5pdGc6MaC7QU9OznLR-;RNd0XV|%u z{2eF{5^a;t2p^tyMJk`%pBloGsfxE`YbAoB)|4 zvW;3I2;uYlls0`*xWf<_@2gHM5p-_?px%M%bO=aD*B0}6419@R3;27s%Z|X0?8nKi zxkwzLRAwk(Y;XGJxA$e#VgE+R5Br)LEdvvii;uh`ZQ&lM*jpjkWl;1IT{XNU%NB^ttnE$D6iC&@!cqdRVMnU~G6GF!@MM0MAEaDXYN}TRW+>GxaP}>i@uY%o4pcp%(@?MI|Js2jRyvK( z0n{tPu6y`nWd9>$%2=gUadxU%hCR5rs}Xq85dLFM9=QMMmM!zav1hD0Wp9BgTH&1$ zd*5DtSjq}xP5NhtklHt|ZGAUS`=x}>^P_@Eow}>P;{QQ2UIu*vs5w~kI&BRB2RvSJ zX?Qx=GMWTx5M=*{&obTH#5aGLDI(yA1Oji; zi+K3&n1Bu0dm9G*f-$N4H}E>oPW1nGv8I1#3Q%rsr`T z%RX@7xV5{$>!p^>f5otjVZbl_*|@sq%^5wTR+Y--2LU!d`>HG*&|f%;i-NbTXL~L0 z>hZWh?#uK$AZ$p$5C$DLpDVRWdcVCAM`PJQ!X%rUngIXDC|NPuin@<`KiuG z=+6+)ZBn6e2cdXXauNnm_KvvHu@k6)fsoPQVya-C?EooB8RqRqmI zH*3KL95k-_Xs~w^BrEG5eQ!c;obCnH(7p_`cx+5mUZ`;%7&FQpFXX;RWq*S9BtG-P zP)#JbqO>K{)1vyd`gKC4z z5YS|vqSJ?bC6_R^RyX)u>nOUKnPYRx%9O8=h3xP0Y=~Wd$dQ;oQUfeJQhmc#(%>*O zA5-@=k=L^CJ0BIHtkiexr}w12r*OY=2kI^O+hMs!)z-rN^IbDup?FG67%^0oJZWqt!R zlr{rkO!t}85;0{Te4npjDMXbKtgs;=`cZR}AF=~4G!X(e zrYY~jr)mszXFIc)g)RriORudXM6@V2T09NovyzJr4X#^NM5uj4E}ZM66xg}K7!Fse zioJqFod9l-2%ej`kca=d%S)A4JzZ)&dE|5qTCYXzL)60$V>0Sn8J6ZzO`S zD36r*HoxiZdI~r)0w8H_>!@I88mcblKJ{LvfyR(i@T`ya=rbB?!Gt@M?>5oLYh}GI zX44xJQRZDk-~o1_Ab&ND;F{S%>n_!nPFrLut)~gkhBmAAH}4p}@jhiRT=F^gTvcGX zi>8(kr_4_;t6i|NyWLDsj#x~v`=)5Zq-$u{QAuoSs|j3Mv~N3PZMjed-yhZq%zUKu zzWAyOUUnh3ULW;+I$vuhc1u+HV@4Bzf=gf%ZpbUkO9nw#*bhv0wAcLY>*_Ne`{4Y? z%$1rn(g^LAh|J219l`qv_mVqs$IXY|QF5_Tf1lB~DSCav!K7IH&OdJOEjsfOU`H7R zn%ytW*lm$fL3nbuR%qVvRQYVyN($RTc`de3HIwba$hjTube}uRx(RtEbsQ*$jU4S2m6hsU_w*Zh9&WNW z9aKhN7QD|U!{XnfQZ%Fe#5b@R&vYE7D684_u*qg_{?VL80wxC1&zOF|+I%9;<9&CL zbStjpHwXKEBnyk@wo?Td5%m`>J8?qaac{ z=x8djK$yy!*P)3LWJBf@sstQgErVC5ZoDs%%zkm?rmAx*%Ms()FZ5<%{;pj|+`qQGi4&s&~nMM3b@C2oo!wUws(?AFS52!oGcA z(qHj>Tq6hC+!j$#GqVL95{Ou@76wabdn{n27jG77K6~7l4(Hdc<1gzSmN=RZ*Dde$ znjOaZx-m@)x#_e}^<5I2(v-X-u!HiiP1=uL^^P1Yo|pwZLFY{-7|*k>Rn_M&nFt?y zYM>;&+2d{Ik5rMYe%-QqdK0Wq*-wz{IpQ~F^{q$pH8V}{?OCadngbTS4^U%Zr8M)7 zAxgOVbRA5nbDLCGF(*R?3g|ri)+cyA1X=MqSLAWtQ9-r3J04(kvp^eP&&MG`iR&(<-PhOG&&$L#Xx12cj~;9ktbr} z6djgva7}-xC>0}A`}wBi^Bi|I!p7+QJqyg=u439Ne zs~!(Vm=cg4aBF2>UxmHw<*I8vd~&{aGfcl-x(4fXc&dM$n6F@?1|0ieayJ zL=bK3je}9x+W4lI?{_4xd+K+OQm^xx=%q1SnOL~GN_fpeNL$8!5}i3b>b7m&{VZ#V zj+i0Yizi8af_-)>QvzMPJ@$oUIyJG`v(u!^-hn3%Axj&tD1%?>+VFN@B)44&r?E!O z2;>`zFu%3=M9KyH_|iBjt%>scPMU9tN1O6~=qQhtH*DR|`gvD-t^q03P*c1ki4wSf z5_o5vx6d?G7|B4^F&rZaiYbl6a_R!k9yS<7P&XIs+aAg2@M5hH${qX$=XE~QCx+rS zF%pC@)+`CFtCohKwd-u|eDh>$xrovA*v(~{Uh^o-jsKyyE#&vJ94eYO+sd=V1-z z#3U>-*{>Tw#v~m>&l(aK8sW;+aI?5reRmU}J`?2$4UMm_3A9bHe_ulOdhO@Ekz+%n zJAZeF60XQ(CV&lbNdA(nhP~w!Qn{{?xJuPa4%WMY5nDeY<52$o7b3ktFoF0r^RhK& zj(y=I9HwQUz@OG+^3KPFbm@>?;$_L@UuV`oLjmCyHDtAHk}aA}RkbqcNM)ki@=lzR zsj)!^E!MSumi031p4j;7imYCsR+Gp^a_8{cR(c(1lrO4{eSX<>6ajn>kJ~&FwNzJ2cIbM0MpHQ5usH%= z)}EcZHaYE9#sJHB7oA@JDn#sagTNamuwc0lc|TMj4-O*A6#vpN@R0498V*Hj6>m+n zPit68IjByal7bG23-thgW-%WXMdzykKql2p?|B9q2|B5QBu-gHP>eM#(= z-mN~h+b%E2BQ!gTzZG-TV@0;5+KxE6riORqzOkFv-H@v7(m54!F6~RGn8-V+?*`dF zvcFb7andWUxxXt)`{p_p8P?R0B$k2E)W zK2|)$oYyNv8gOv2@=)y^`aIYub*-#wO&yl+_9a<5yHC)J2+fk&onsJOSm(aCTU&$w zk(KJR$>bMph_3}%M{Z)-<<|9WC*k(U7HH2 zf-b1+xEz3r+i}Y%aw`W-w5~4?2efznxvHQ#I5ORcz;UzWL{R%Ta=%JBDlMjVD^xgC zOlzUln;srjw661ps|9O;0l(trvw@@2Udl4Sj(EnSH7Xobc+>+fLm5mgRg9DRrSF2eb#{h{PYfW5>d3SArc%96;qm|AS zcYa`IHlf@`T6-~+sP1mBs7Z#>`>h)8=cV+g3q~G?kE3N|)T2+(kdJXnVBoe^enrBW z1Lo0B8t2ZN{G))mLLE+Vvl=d$*5)I1Gfy{8Z1g++TcID9>35GZ*AaIWq+#Pn-o-mx zR>lR+`7w_X#dH~;PZp8d_zI<2sz$H>hUPEf6J13rJ-!Hyp%29p& zAJMxvGfk7rw%3th-xly^Puqj)A8(;a0>_bh=(LnQF68AvozKNL&(ny)_7^Lc`Niu& z=yoZh1wUp^c57t>k9rgnu_vlBM|)c3yb%VddQOecn=v{tPF(|(emi6?=nhiiaX&KwpLP?@vK2(NBX>g8BhS2g%ddBzyX)P^3b@Sa zyG_Z*m@Q|>@@HithP0*jbv0T@rg7_x!^KMOPh04if!=Z;t3WOtw(N3tz=kv#}rcpClym?Y)c}N_lpJF?#G3VmzvXvShGScOmQKs zPs5}~cZ)sp8g_E_?MDNYC29>y{NtN)jb)wXRM+Rmn$8m2$&avs*pRp4^CPFOA=P2j zdBPIm3Zz<#kHN0BcH_boRBI1cb!`}k9Sg&9jHw$d9!2;2%*7$aje9>1KWH(@*&#@W z6RkmWYtq^&1k+eis$6j;soEt+h3lr(M^-NT1`rgw>PLO9svLKpZhA!;*?eyY#FzInSbQO?vn_Q z^*V03Hn@%}^UAF>_#}&*S;{6iFIii8c}dTl{C)-s+@~DvjKZg(^7xc)zhLV|`1jRH zFZ-toKXhWbeJ6$fWWIhp_BE8LaQZ8&^NRtn13&PI?nU5KU{cL-H*%;d^(Pfc?Y(S} zUEXZmshGbDi)T}~s|FuvPnbn?B6+;w>8^^WWic2Zo_^bM*%4dL))b{0wDKc;$}koU zg^6>kD-nMgZ4axf9=wvgMrDz!?NTTcE&%zVxm*HnAx%2(6N+~p%&mt>wqI#j?(2*B za^a^zE^;=rp5}2XQMZJ1UM~jkxk_J(4I#01r>^FTZxp~^$xDM$W`}mv37blu$++(LWV?xftg3tOhE4!7*I^b=t^%>-cx$fG6J1%1|fwAhlQy+Ye5g;iwJ z%8Wc_PhX?u%TDO_5)u57@hcU*%rL}u-qcdVVu{s3CxaY=jY+w#db(!Q!(ozl!XYtN z{lF5Q5*F6S!{nnzkv4KW3Ag*BnXpFX(nitlm<8`BMdy6O+aIJN70Gy!_miEj`dL{@ zxZ?wKJw`09i?nGD$E~ZWWhQP?WX+T7cK*{%`W?nzu1f*C?dk_* zPy8cdD3UYRtPtfDaP2IwV)*^UGt17^ij{Mepz}|!6>smVSw=GsC0c{$W@P{*hQjfO z#znA-x7(d2I=9DfUdUtI6aMiPtoTwWW7^J$@cjV{Lb8YroGG?5l~zlV=h3{)LX^l8 zFSANIG+C@0;v5VZE`TNx!xT!Biu~`AXrl2dbEuDs%3Aj*yiUY=fw6(C2#DAhnS`0) z>v^h2jO6V*yZPyuChho|dK_9i6PF6|u>@D`$dmzW>IPn?-Jb+Dlff$&umXz8X(yE- zplY@mE-3H?mdn*IISvMvneN5Wmyo0S?qmipK~oeQ=8S0|W>r5{ML?~VbxBP}6j^^l zcZ60$|AKM>S`MKPuhFeBG(&ZW22M!M`cbMg6_<`4BBRIjss9ouz2)BKSK(VXK5;x~CmZ!%6J(rxcLp!&l>yuPb; z@Fo7EmudIs+E~QbqrAHlboNc56w5x1dhT}_I}^PtG(B;Ak)MRT6U~z=K5r*gycwKB z;lqt#ID^U_FY8kwdXUs@_rgv2I3IChVu4-z@NN$q##Lzq&8}qIT5Z`E9B`bA&@}DX zA@#B$#_QG*py|~IO(1bv-{Hl$YXHeEHyX0i0Gve0Zc0^Oe2^e~k!;kUh&w3B3U)`= z9KPIAFHsgrkEjKy3JE`)bdDhxzJYKUH+!vp$1@DxBu=MGEmp6O6=#B_23g(LE!_!?8_V(iQ7VkBcB-Y`631>hq<{(F(zZTq0il5Ql0{yj1y zzZLd85zp>(eSGhPRs55o0_7;T_3-UhGF$Wy68%B^-DeIv$K~Yx8ffk^y6pS>xHhlj zG|W{r6Dkz~x@!oZrBOY;DVW@8NUtG)2y_Ty7B&FzwCaXCBW9QJ;yaQ2=ZErtDg2%OUL;#SBL|tq2sM9ZU=N4X5FUPlpl=+&kPdO%d$M2@6EeK^NrH-*eqUUv*DE_90)j0`5w5RJbeT29JGu6 zu+{wP65B)A90pk3o4#zy7yM8`Q1^Tl)?n{dy1jC{jMa7=+4`$H!|JGE+o3L{MJEtF z4;smEAV=QLSpsL~YypBG@aq{J(5^?{NL8pi=&|yfOp|jr{ayNq?l1<~Sc_&!D9CWp zf*QG@w5}Vfs_JAMhJrjqr`4H^siyA^mT)nIV3n9DZEB`9j-U+ag$X@(9=5E2;752Q zwR^S53S-c#(m*5(5i~OL#SAB1Wj=BjU`kA-1f`bTAMB=6s+xTo=<{BoEcLb z#AUAN{4CEBmtVC^8sWqg>Qa21Z4Q-Xy>(m9`-|~Ad&?o4T-ib83mIs zLBZq#KYh9&!XL(vjXaUhxED3>TmznokS0!pBP;N z=;ZHxwgpxT`L6a))vlMN(+QWAE_QY8TevRfp&IL|(Ri@CbU?-V;_6ea!1>S{cn+zK za|$jF%>4ox3ill26!D$ei)&^{3w9IqubiB$-j_v&!!}T~2v>agJt?g|{d*w^^m&)! z`rk*bLK)6JdgCyCtL7RN31IcP2o>E~`~J9c-I^dDz}yr;ZyyOr*YekMnO1N2_G|6c z>Z~70NFOq?j4}5TM>b~lmI)v<9U|3n77^UZ@E0eh201-Jjj3161gC!yYcOpxhwQZC zJ&=qIjw-X_NMeU{U7B-5qI8f*L2`EW>8h@r;`2`eG$%yEXHM2`r|Pch$MhQ1eevQI zjxq@|pfFzl#Ivz?G!nHehwC0;J>Vs=h!)%FlKEH8yS9=1x>@UDG$QjfkG;YlT<59^ zEcn9pYq?I?z?9lP)RkY#=>_!BX~dAPk5`MMM);)jQ0a4tK0yy>g!*k2cNx9l^H~$h z*;_gY^Tw$T|6$s{bm?`)LXVuUX1&MS9V=*G6#>OC7VVDyAzfehZyqhf36o#xP!&*F zm+}e-MrK3m7TxvQJRTYCZ<9+z?A_0Lq6X7>yjN~aA?ZBN)i=;7(aDRv!2Cn|x7M94 z)0dN%GcF~XB0WsZ7wkRk8q+wo{frc|R=Huv`1Cz%oCkS}y@87B=7!(b8K6oFOXEof zyV+6wylma4x~zCQN~OoX$2)rnS-iRdrNDAqlmqoLRBqX>eO&MR_2Qee%>|BqQk~k_ z&3UZyDWt%H?T0fia21vW6gaztcFCN0 z!Hc)n^$pu+jED$4D^Y@r`U&23+EZ;&dyZPh?nmm%DoJlAOPVhL*SpgM4!U;DDt(*U zlu!4*El_FNsM6E(0ln^~PizG^2xr$jOtAZ&K-TL`i}1!=OT1&})^X4C74nKx-O~Ic zHed2a`d!zl?J?WB=Owi4ME(-CLl1z0((R%h{vezs!aXl$s|JsoQJzJ&Hz-YoApw?KKv5 z>gw7cM_xPL*E0f>UJb}@IFOIm^DaRRie63hp}rCLOOU6{w9|H>BdEmFG>(Sh%;W|x z`B-&RT&pC%cw}_fBA;!lSV`0y?BM0lf9$&Q>KsJZ(X?P)+Q1NC-?*FCZVgA-`sGB| za0MToQh4dKRW#sQ1xnDq4hBUw^VsF#rNUq6P#+5#%xLMOrDt6#OqOi3k>{`IST|g}TxD@q>vF z9seGAn@^u{Bp(`|%d_c&SEFbY-DIBcfh+GFYLxYLi4&GSYdzQIYcy;{JS%aM(ms$} z8iPpl4L!Kmf0=GOwppr<((tMB-eIrTXr*O$NPRg&Te6ndJO1n=i(S z`8(q>eV=VUSH9P=_CKgyv~yatsZtDl$adK|Pjy{*6uc z%`7H@0<(GOTltHx7d4T|HkERL7ji2A#y#x!*1MC}ZmheS>#wy}C$|BarLA{Xy-^)n zZ%+dhE$##lv(hoBd;B;ws5dFJjOUpYi5LSUVdSDtX=>p#a+c{P^)p~74Kv+c&_wz%I zA)se>LkAZ!*8T7aU%jH|=C{PQX4jl1O2Pl@6TJA_E#;7|=%0ZvH3xx9vC6{4Y4DGn z46j7eFhV{71SceM-H9X$u@EwKI(V@P1?2|pjQjmLP~ox|zV{1x=dhHvTrEtwh^L*R zwuCFXhk&Bl+HShpf)b2s29k0qu6ev!AZyO?L5j5viI;(&~sC&Cc&ui#BD1$#_ zIy4{RYJ@fZb(ORRLEo#3szV2H+4MzQ@Qt;kmM){W+E@syf-#hB9tM7DCXFS83_s8` zlUtSYKwc`hD1I~RwMTxJgDy_E|xt}^r@-@|gV)yA&BhWwrz&V3>MDO=xkQw~l3 za0v`u8M3vDXj#vNo31g#)+0V!XFzWHPJyHd*n*RA!h8z3aMTYVLeAW~Q7~|XiplCz z>f|5}*R)l10|rN-yS>H9C0KJoQrVB{sAl@V%=U}@<6W4Tx#*%BMjE%Kb1bOQXY=Zh z2AH6IZ76W_o@ci+YAM7BekmfqgrqX*?JX^j3B_@DZ|ud{mzCxfhmy=A3gxF{zA(W& z7qL124a6P*z*u`Q7RA@`WLd$4}%q`J??3tP$Eb{5bhoHcE zqLZ9!El>*0tkgD4OKoW#X4Ihej28L+^sLl=zDxJE2j=AU-mp2pUG_F~A`e|ij(4g< zw0HK@*>GYl;-gx%L8Ws4`{x=o4ps)7&)A97fT0?^Zq@xCV4s6Pwnkr19G=S4#?B>> z=bC%5!H@atD!@3Ms|IkXtN+b150vgJzZK2#;c`VsjwqiV#8-*T6j zD_9K6E%K8T`OJwei%(8EeA2>901AEFDL;np_vsqp_Z$S+f?(p6yQBz6g|&2WnaFGo zD#ioL{VwiTpIgn$pu%Am5iUj!GxyAVI1mRih>ikB!~CuS9qubtJaZQ>v&ly>WS}3) zj8b20nOHfeGj!Ti6U9T7bv+Loa`OWLn1Vu@ZFoDN`8llJJk6cqqu)Ipv6@o>=pBB}#~$5|)n;$}{L zfC`=9`d)lL3wWc~1d6UkPWt6k6p~CncsBKGL4NfwEh|LfGZhe+ydNyu7f_N@e4p=E zB^I+3>MTh(FpHG;OPfkb1zc@ZP>oEX(^42lI~iD&)FRONO^0;OH3OVMDwO!59R(E9 zoazRYnd9=EfU#{Vu`O)w;0jgx%0;^-L^QjYx`1t=%6j-JsBBvmgZCLrYm%{it4C86 zXoOAHE>@f+OQR^bhMdbx)u3$nvxNE3X;kPsfG$baUv~={)uh!fdBxLOpp#`1yac!v z9oe}SO`)aIcNsY)E;jfarNtlwAl7JzmT|}4NTk5g4wdz7}McNbVXH)64Ub=DH z5|$pWJLtb>K2tHrOB4IIUF5PM#$XLeVIe1Yy-#paySoZFl;;|xQS(KN(PQq`NZsIl1$ zXY8$MkV4I$VXd26Nqj}qRK3e5Ds~quE;W7Y|!Z&VWn0cd7l&g=~ zL1)I7XmB5x-Fq3&B-3Su!a#sd6AI>uKQ#HlvX`cdSC}(nSxO5_G-g`$O3QIVjuDY5 zE~Xbpff+kwAzU+haR*B)7T4&Yo1~&xn_lnQmssMQ-S*~e0o;TJ|RHE--B+19jJyNGp2r{I zf^_K2(bs9fFPw++Q=4v{2c!M z0vD81sOl1vqqJ^PXRk)9K}q5BMC zC5=PPcG)$-NJR|KH=_v1mYD6I=i@|l__W^KZ~Lw~QMv&aO296sdQl_P(Tv zCTLdTK;{s7H2Jl%=9(Z?ud zN2p@BYye$QvDm#dlgvD18Qt8)TRg9Sy?x{zB(iiGnamkBg)H4ERERkUN~Ln-qmbbe{$BmZsI8JGQx0ZN8ECK_Bipj zk0|pr$d44cZ$G1MlkZ4jv^3Tr;0m{1wCA6|py0jN*Nu1XvV^Yf12<4AB$iS4IsKz|hQMDqGnEUf(h|q5F;_KI&S?Hc#w$-}J zu^m;i1T;y||D1<+Ivy*R9bZ*fPj3@ABHOgNIlxBYYnvR#ZGp|nn~Mud_|0Bn#amws z@9xSs8oIAP%C<3xhZAY~(VvG7bvghS#1xw$Ef+g)%q zUP|%?XXLgatWmIP8;1epBw?84MaghD;kzh892+qKh2=<}5%~iYMVYlkaV9=n3lgk~ z3x2+kcBlSsY1BzMDEqDlCAkl0$TV&Y{J$D4?w6ERq-#NN)EFTmmmpa8MLSj z%gGm_vg}SiTtZ%r)6gkAgAeke-WN?86Hk8_G8cvzxSL)fq(Z0v@1}k)LWoYe6D_93 zVgYDU<%Ez*=&b@V*a?^uSz0b8;y#v!HLJnP?1?pnh3s=MQ-28qHY8kz#J!w%?nxq$ zB-+bI>_C?@fc*O+j*&ktGt0U%AC9g@K`s;=%CYx2cTqB+-r=B3B1V*F)hv6 zt|)dLTtvxEWC6)7c)K!@)Wx%95Z`MZBfo<=t{Oh*=2bqjPJ9YbZ?o-&x}!V(8G8ihkD)F?QP22k*E{hKM@K5bh#`U zLy39?JG~@45I(bXq4XHC43z-{yOIE=pV@y;Z|qpC2|^KfRrFmbYFT)47k*Kx;Y|mj zUJQM4*x-e~Y9~&Z0{n$qHSWfJe z^o{9I3(D^J@PJ7uPUbd>P^ODeiOu@j1ml}@;)D~Lsqyf@g1W-|qsUBe%H^@|vjCvP zxm8Q1dy`j^-Q;3td`*xdcHroFV>)@CYY&YtO*oRBvC<%{t$E8BX@>AZ;cK;P{Joq^ zkJ*_76@|ZfGk2VGaf5y9?Un;^Z2sjj1ulzy|E!()Mw_7I$&&Cr9jhwlv7KLNV z*Yy}cHM>N{8^9RCr+R6RPU_a#*)m8(qQ~)W5_uc>sY$F{)8wmcksWZw@Is%B4D-Fy zcM@{rb;S{)PLEL=vNoR5FuPK-S5-cF&JUbvnk8(z&G?rJ1De+n%$MJ8w0p^x%}MDE zx#h!~_SdeLIS7aDQny3pG}G%HyFEzgifnyO>V3ge5d-wS#XIqaS@-b!+by}3ElLL~ zdgMo;@7e_2@p&u=KqzO6+%kd&6_Kt%btH^-suc{ilN`&G&sfBQ`S*~9B1W+^K4vD4 zBQE7%MW&DMIG4QO?GbHfZAep+i0ZyE&ZL6p9PtyX02p;b>!DxN2`5-V~?u$xf*=8rpx%bpRkw4Q=3;O;(P*{=AqMy+)JHzRNja@rQg7V z$6(!4$X2&Z37{A)DCBANDPM$CugkGzaw`nsy4c8kWH88*bte1Ib5 z;P^e#mGq33aYQ*!%ocI32rk4#4~a>Ldv>4D<7n2}i9B+IGY8*EPscm#>Qd(t3XoEl zJB^dbH#gF{%nBW%`Ju-N9AJOlagPeZPZp|V+bxRWlcRbsKX!;LWKIY0>W3@%-sdrX zYAf;o%w#XSV0g3Pz+*Pvu6}(H$dI+>;p5jQ)DRgaHC?xI$gTBZFz$5@SD$s?D5l{` zN#jAYQOn72Ck2+B;bXjZq68r^he{FFcB5dgx97XSdRFY)k85si(r#;)h19Z9{s6x8@#_NK}~$h&auF^hNmWrlI^q?aj@atW=1K6a-3|`buqqcG*w~ zOxxAvLlp@IC0VMTTlnIKFT*tZk)*_H={6O5uosd2T((E|ZbLp3^g{u%f=797#l*T( z^1L*J;fn&&%os}L6R2iQ*h6#}!nBBUUng0XPFniI2vLF&-Q%U0=W2f!b9-YT@D5F* z_%|oGniOwInLw#QJ)z9U%@;rHGQTf)A&JJ5IhFYutu{X;XGW8&@iK9d_C!i-NawTS zoyAaJwX}7zJr=L@P58X;%>K1dJ}FEL46 z;OzUq_aBwtt5q}1mZZN?y{Ib8XIk+6Um@i>ib*HO!ArhOQK*u6l8oDaqsEfMJciNJ z`T!PoNTtaIP{^17G)aAG?* zcQaQWp~t7cvX9^|^YR@u0SN}OreMduZu7+2^G`e%vB&mFZvdk6q~|tHla1JC_2wP#uLi1= zqMyBp$!#17y5j)fD1NdO`niQ*WJ7eQq{s;z5!-ORrwSJS` zj0IoslYV;K?|!iJ?bu%#N>MXEh}Td*7=A30k{(y-|B?7<|EF!_!t3C#4+=3@A^yG1 zVBOKc9ZH2Uw@T=bG3eziSHyikCwZhP2y-|LlNJa1bG^Z%T0?F%ie-Jkoh4?)+VEtz zcaQ5&*_h^p`3$9U04=1Q%9&obMRX}m5v5Y4OFNXZ@dpCYzPF*>A$=wU{R~f zU=jEp{8GW2MF-cc=5y>;84&1lscHA9WF)>dgP@%XS^C@=xp`t9$k`(At0+E(3mNor z2GgLS)|!+s{+}TO&S6qk8FQW87s&|E&aBe7=Zz^$am>MW@v>3h7}6p75uA)H8_OPF zvFJMPw8U*P**yoy=r4-g!R3+2OX7RMHx&VtU#jr6qnv_tmH<~Bp;jK!IEO}YRuPCO zRfe<1Emoz*TYL`r8-}7HVtU&HnM$;9dfYBTfytZ%mN| zcqUc*2;nlFOq>7xBo@dMv#1wBmJ(qF??)2tSj#MUV>`K@I#al((PO02@T(LPb@_N6 zGNdhhOO>XTrSpWEa)#gUgA72pgYjCG-`)=@E6qt@f>OPOpz|5xWV^78aTWh|Bf{)H zGN~A+_XKsRaF`V0nN>Ha-+lV5p-0Dp@Fi`?asgdX+TGtE5t3q(?d;W9-y`TTUUtpd?o;Mddi^h-osP1l8(P3%;O z&Cd&ytx|=}qL#RVDy*o^tqzOBD}ZJ4{24Zu!B(xUq5PV9q_O<-36FZEJ)Kq`dC9@B zPlP3;(ki^W-|d&7hcHU$7M{4$-z=HNBYQdyK$KOaX-57lQVjM@*F}ns#d|}O-zHCK zZP;K9hV1v%n0$Xxj#6t=C@@*6X89_w{WEz@-^j1O5_WBn7|9d_p%nkg0@(Z*v$UqD zJgkc<1a=6``TV@O8xzL{jbDC#Kiv=kc$!$rWT+xL#C%7PD5oxZ3B1i1F}XsI$(+jA zBU$0L01@9M)wZ;8eQ)}}>Vc#PWtKXk%^ zW3l=WOoxsOcMoA2>))(@_RU=st$nc<3dY^pGH;BpeaM(&$v;G{XSae*@7ttpvK6vlr7GIW>mQD40^;Ia&%@J~gR4BGdOSl;ZWAZQW}ItsTgX>h!kBu6b9y;VW4UkF9Q!O27ndz{kJ<(C~>(MLs}kTows>*3L#Bn zyuPz$yUWD0(4+wcss_5>78tLS5F(NsiFm@LMD!x`lCax%t-`x%ahvE}=n3K-$n;{@ zQFjP#l%-@`tRy|LbSA?=s9~~jsj(!7lX^l!i5myNJoeL4o7EWwC)tD0m4eBggILM;yV6tEdptRs zuhxbYDCjZaA@a5X@m>S#SL*|e<`t9VTPwFx%eD+{cXq~JYdoM;UwJACqo+O?j))qM z8ZL9H-v&md7<|n*zR04o+g&bV6XMm5NTBJo;x|Pv=8=Z83ZA@qG4YpZBFcCnd4@je{3uZia1q~xxK zFdTa&kh7XAiddijI^dd63W>+A+FBwTdNOq2ugec~24Pl%(m&f?0XN<&@ummMA}GjP z+hu(boBH}z-8$p#+jLY%CMI;KSC}#gF`Q(7iSqUAytp(5|MJ@nbS`6=?ZX*!r6Qv! z8wUOOElndGMK=VMLVOtPx#Z^EiY-`U#2aasw5}%39Y-}{d%u8N)YoS7$vt@Boy7JGjzTewa4 zDekj04gd;~sNf-Rp~QSjuE039z#HZTN$M!jiRY7v3UY4$@H7|icCHEWINhx`i)xXs z(RcB1?mesr6`)>4$btX!_I>)A@XohHc$3}wokF&^()}H4~i&XUSVP1OKjA}Q= z_g1JF3L)kl;;W3YVhr#S4wN}F>eZj&JOvz?ClO0LcihS$46;k-xa`fd`-jr|gKVJ9 z`+IZ3BN&DyLl*y*r{d=_Zn7whhjox&mKo;M?at0r>B@{UT})^Vy6le3}E3wljFV4}=3;WUwDK1Pl%VW#+E2Wu; zReA{Chpboh6Xm2xN4;QmiibQLTjn}mKPwCP`!&#oh{;?+EIfoKWlvoqla9+L1|NU@ zsx0W*5-Amf(tqNVN08RV`>w2(Vlw?-M?&{r1au^FU~F2~blq>RM*P_O5QPdju;`9G zu$i>Epy9tK{H3SRvKjhjweN#J3t2(I^`W=qQc`HPo$E5^Xli`CH9D#Ex9)K)IP{bJ z5|cjc{bB{nd8IV!BECoj>?a_$n{k^rqBIbT_r7z&U&mg{6(0V^Xm+uoz2}ycs!o4# z`VFi`hocXGV`qA1ROU35T!HRniRh8zm}sqGjk3jyJA)fB^Ile=881JAJNBxTv+W9E zPMjIN+(kyg)TJ)=n5Tj=d4BFPMs)Xdl88t7?)g&DhvvM*dk_wi%gu&rj0t{g&bu#` zgPaP@Spp_ZO4^^FAzKOLq9y#|B_sqQ$-5=6X)_MgacnLb&MUeP2zIODh}DHY_suBy zv7)+cv(3eDx`m98{9;y*KZ05SLq2gTchO{YVNe6X79|RmX7SU{P>lWVFEuPlzeh}{ z{HfyqPtWkm?V`DilvJ;(BB_<)7zlcvKoXF<)#1_?oj>QzUd~^ zS=vjvETcG_a5|Y>A#3@jjzfmkbRSI_a28;uF@@t)|+K^*LwMB z07whYI=}A1!q0$!yq|J5d2wjwoFcFg`cN~)>ubi6ad0X||qfVo(mwScj?i30`8`##9 zz&j?x>t4i{P7p|6j0QXY;xN>=2B+EzsOf5iCo%}o*~^z=ky2VIuvAMKj^KEK-M^jq zCKvqgC`gyBkG|Ec)p58J7U2SZ`e2-4Glf5S=n6*cJHS7ASj-@qO;^bKaFL4w>vekK z?A5_~p6b{wtVZ0OHh^dc_gftL^@f_2PTD}cMS+d?1z|CxOF64K_!vvI3^lj-*(PS! z*#Sow>+OBxixGz=OA@UnHTGNQzJ5O6dsa3XN*jImi6f^8mQ}BFMy}{?;N8$o!Cl+= z%iFR&F+|C_u09<%>>(i9`lEDq@OC7}g>0Ksc_^%Ta;7hZ`sOS~PgBVRdM;hJHS4*- zY>RY9!rA}?QqEgsidGg&*K5;vne40=%MT#03wbsv|I@w0OCYrrB}X`|NBI>sBDp*1$v@ zm1blY8AR(saGOw|?I(66Gf+t=P3WdCD%T5gpf1+-lrX;!vhiP*BgP2amfTV*ZX2wd zSaGeb(;j>Ixc!wlZYyf;woIgM?2gB%T*2R{A*0b_gT~VbKf{cbM(;zZK2TTP4_#Qi z`Qnkjdr=RLa~Lpw@XoIByecwo-jWFX;JN4ZD^)x{$;r@=`>A=JQ(V4=Nb)~r?@ZD7 zb5wzjmtd1X7!R91w?|4h5Fgy)iD^Qm5~1`X_`8#QALJI0&WRWgA2cJ`56&zJ=aT;< z|8#K6W`gL?V8ZP3NFWAhJ83x%5VIH7gu_syBMua`PkST=6Kd(T6*<~H2?=WM^}E3{ z$WiKmuGFU4iAeG^Trq-IF09#bUTYBE{zrqrRvmxmmR;e4o}^$`O|qMJ`~?1VY2M3- zJ}{jYmo}w8TQdOJ!S{5vlUU>a>h`N=Brjvh z;eb!aSqf5)-#P^Ps?s;TckI($kQ~afBAdk#1-Tr1HeFW10Pu=2Hq8uvauD(6m4W<7YV7BRd zJF2%``&AGnY^u@s#P9G2kxxd!u#d;*FEDQGl;>Lb49wVCm%(GwXJNNLUCKs9;^C+J z`v5Zt_Vcq5<|;0x31<30D5om6&5P60&Qx-@E)reDwYr$zQ^!U{76qNY#&1RzW5~B& z>hBYH=mSb>$6;JsH)P4K?cz@onRe5;(i#M1wiB>jH>$3^@}*^&*vWuB9A-Xfbu;+> zRRqB@)aFC=nSO8U_i9;Z87YyqOeeMpCuYMK#85C*`Y$MQ`!Yu%C-EbX7H)T39o+%_ zb;G_>el!i76I9a?ywB7l0O>HA53E3cdF}T31#L*sUn06J#Eqru-hMA;hI|ikDD8FW z3|)z;H!4G!Y9}4h)$}k#Y8k!{y>a$0y~dyZjI8L*o`j0IgA4`TG#KE7CfHS`edS99cr%RJ^p8?3 znechk53SH&@BZ|O#4xTkT?tIXuT^&M0;hsA>vl)%g7`mta5_l=%;SDfupU>Nj)`3> z+md!7y}AqciR9bfS3$EQgoJyE?8Y#-M?7=9#Ucxu z<8SO5@$leV>7=BU0pMjb$VRH}h1JY?ZfQ$cqfVQ>--x~KA#g)cSZX0bk59V%jvT3; z_#qzV0a8Hj1ye7Ht%RrHJ7vm!VzpyjZd>`dI#nX9KB&}WIFR1of8F?dR+JxQFrZi; zkWq7U#-IduTHYbu?IRq<{B~rb^4{;O?nTjysW3cNz+(?AS}%6#RNK_3*m1Ru*uvOo z=nM0RkWLHCToD4H=1zZ%(J<%pexw^GgML{VDUV%8XIgvoCZNA8_czAL9IQ%XS&h$! z!kfq4vnzD78GTO!iMQOY{coZNpUg`mtU3{FLcB)6_)4rg{xP1$9Xj?nv6O#Gn+OY% z2&mE4IA`|PQ}UFyD^*oQDvX^%t#ebrqy{v>byb?V1vEg~{;Eu#4iV5x^WI-3@mqlrM_e|Y z51pLR7nDwNRpK3r%H;JLX!|B$sV$j3Rnr%6G-ynhlUhF&z{6pQw7P(ieodgal0F#H^~R z3v?T}H}5cH($HSlL>?+f_CalNqG#rR_hS916sa;RYx>wobZZP5J!w>GP*c>UXMO-} z@l=m37u#^`T0@M!{r&>FRka6sW$z6&9P>f`Bwg;y9)-RisfZ+4rt7?y78XlFM~%0q zzt8T)L5Othl;hnRZ@bvBH;wt)RZS{Evk-(kn~diEH_TtQ$c|&C=_&S!NUh|r6e^8x z)jJ|lE#7|@Lth8~jz0lH+52ntmq~I_jQDC)ZQC{dSi`CK@5zf!9Q^AUeRRv*C)mi2 z|CVI>Dm+#kD0|&0LeDg`>%Ar_eS4YkV*dXmg?;kI4%VVi-}*?KPHoaRj8Q-aP?({- zw3+6wfczTP*?&&L8n3isStGcXF;r`AG3crn~jkqiE^5i;3o4H{#rf zXj9nnQFL>8iJ{{tWqSzQ#xx^;~iaparT|lm(5CL1+^1M z-f-BVUSIH##uGoCn<+)vo@}!7<|#&O$*H&y5I_oih0a$(sN1@#nErH}y?r8$I=dZU z#}o0cciHb{4SfgEzDC|$;vI}#o2>%%On}W#Jf^Iw<1Plw6A#FjCp5zB#UFpg0huzC zB)_5yQjA3imT&{2qD4gw0|V*f-|E%S@Nm&sdQk9r9;nTCGJoT?O^foTCzh#mVyIuBkC2Es81)?{gcUZRN=-U6J>^E#y-fGke@S;Wv<_R z4%lm{_`IF^+q&67fAfNB&irV6#uh=0%>7{PFR`vT^HGMd5~8%LdE}*~^!jZAo56-YS?SN68Nf40DNiHQJT`o%zI#6&=d{t8OXGhXpDn#_ z^XK_w$tkm`uFVVar@s2+QyXf=(dLBbP{79U3&2KKu5=9O-iKXof0&%Lzm(vT&9Lt= zY|olvP0pGWKN$(d#JaT_(037wvA=|3og`Fj~UCqAE##?h5!(rt#(>9 z1AUw^XmUR00H5lSewb1y6Tx`(`q%{Jvq^k!j8mBgN=155m046UUZGVT6O=^tcFyCM zV}`G%H2ie`Kt!Pb%5l*B_=ndlwj!oG`Q1ZduZAx1>3y44iEHu!k|n*4AIH&E@&ov2 zc3WH81MSw&otld-kZrS)?Z`uvR;XNKWUMiLonUZqCGCD{hFC$f+WNg56MIM%%`q_A z%&xZijUrEe9Db5y*$??D@w-y5Q>erjL>b)HP;~Da?!a+?AlLHXe{ZdZ6N^*q;Yi{R zpR8j**wm!by_|ypm*)lB!cwtqR;w0s^8>^mgg5IoKkkWeR@Q8httIHpTa3xA0Y`J+ zP~ObTE7zeZTpE$6Q$@s>+C2AuyYjH73l zQM%t+HuJbo^{jG3azA#h$s$>4O8}ZF&$!k|Bjutg{%p%ZR71hPl4i<1wDW_Men3=$ zT-6Wyj435M2}*iY^=A*DU-+;Mh{A&T6ey=|dlVi*Tp={t83wEU*v>l+x-T*;I->;vFEDwy5V#SdgJjgl!Idf)qF8?2 z!3JhJDe<3gf*zm&4nv*Wg)zYU{tt=30Lx=BWDwe}o>OKyfLHqY+7u1>MGVbdNObeI z<_G*-vT%_Sa*G-W6s+!ruiO;YH9S_%9sm|Qw{peQOrb|9NT;o1ScrcvP@Fu-oe>!xF@C$4Ygja0Z!_Ksi_Z{;>)|0vd2z zg+ss2O9xM*c7ow}GzJur{LnC0v>dmNqKA?Q{E&3-;ePU7P1xKU381bJ*DISz&(24l zPQYhZ1=}dNW&+4V`$vt=KZsN;#=lKvtuAPMca{b3bE|;=C<;;AOE}J$rKN)yuW;i1 z$oy#=pwPXF-ZgKwx37Ok2k8?x+z-rKVP8DOCER7j;oiFAscB@AzzaxC-?dr#Ksd>T zap19TOO84B8W@A`?Y5uXcr>W36iDJ(*2{Pc=i&Wbs`w@T#}kubu(5>tTVQZCEUL+t z4JNoAgTyUD&*o|%gwUAvB(vvh2*h^0<#40I;n+2F!F$*#)U%6CUBNTFIQD9OIsHuW zqwmW)<1E>zdv8wtcrZlS&Kw%PirKNbLaCus%eQU#Tgqix}$xU97nrl(qy{b&hr}$hT)Jj&2zM8gn^8Ms~ zI(cZTXHD$YOX!)J!bGIbdMn}be!E~O?frP&Hz$X69In$r#w(xg^cH8H)?G-$LvOKku4JD+h1Mw+W2v;M0fg z1--oTPY{cbnFhL;@7TANtTl7)bxl>w8OUt-#kVndlsPWq$!JW z)rYzo=Er{JvGu5@MVawhWy+~6WzR#ebD_%7Hu6`y14sI*`r;r!?>aQfHT-Sogu zZaC4>@@p=L4ObZ8%6WgtCz@liIVsC}iZ{t%X!i6Cn9iMaPt~6KXe>EwV^b9?p#AzP zbZd`Zhw6kyvB#$zCh!4HIWkshV`*SWNKMp`Y}RnVAe7u*X`Z7@-$?RHEY(q}Eaamn z3MhVVStpV!h*$N6tRk9eeRKNh;__P$2IOYZzQlj?bK0mn__t@F7noFPE zy3Qcb#&vuz&t2W~20kb#-+VN=egb>G5q~|m6n}f}OME$I(xO?d{$i?U@fP}WhMQ)3 z`9oU_b1_I}E^C&KYVI;1d~~xk;W!KFCncm~?|WK0i6$QOTQ3}7Sa?7omSW(sLErv( z@q^7#Cs~T?nwOxXhI>kD`X^!M*2bXKOnYx%Q^B_m^Nd{q3KX3}hleB^k)4Q^DU_Mp zM-RFm&3XX-wCII@uZjGTZ+4x|9cz40GiP==PTqxB_Zs7F&CyyDGX?U8vx949)3G2u zm<+*qH1T`ngOW&1FpoVjA)8&C3o%0kNbbSP9wbxoPZ@MlZwTz!P@vmvdSzr2{#tbk zsbNfP3QolETBajE=TCN1isM~)w^_U({c~_!sjkOgR$G&-(A-3e9Fhth7v8k!6pD6i zIB=~c@4hj#wh}Sl_OpI(?rLiffWE9pjZKF-0h0#y;& z%7%UGMzwwA4u^$F%`N<;%2XOc#Z5WRi=1;V9+Booz6?7suS(>flJ)(Sa4rp*bwULk zcc=-OdFC_Iz=vGk>T@F)-{qPLi6uqvw#KPd!G7Y=ztP)V_D7XODSUHXTCs@iI_>af z&Ws}jEslF2l(}{OH)bEZOGtTj+JIwYKVv!RZ}vOQMpo8ILnpOW3G>5X#l|G}!_LSG zpbM+$C$=Nn^;%L*Y6X&uXFSHqQuZO${q|^`LyslnfvJ%INcm~2NJ5yV;=?eBGt2wBZ;0nzHG__+jA_O-cHgMm8p27@^XNbQ@= zLO8?gFB(W54d&9!5w5^;*3ViUM@o9jVe?a!D{|TO+3dT%71k}sEWcbnvaN~PYx-(L zl?HDmhz+XDx$f)Cu$_28Gk*viG{V`mhNX?3*vCI>Q#3lNb-7NBliT$K@CN&yQa6Mo z(oI*7q}u>DDo@ME%SJEh?_o;5M>p{r8%#{Efz8L8#}17Ex8(1XS!~yTbW-gRX?97bZ~sOkETUUi%hImN$Ovblk` zSf3lZ_mx z(T5cs%S$dvvsb_|^()*V=@0pH3)Pe>#X^NFX8)ra-#=o#FYWlf5A&}g)RC=1zrWt1 zoOp_N5qr#~0jXFwc@uEEsU_00(e^*V!RZkib^%`W}CE35_aS`RliS>Pl>< z^Lbd57bkfwv#9QRE`{_O?Y0Oai6%-C(tOu4_9d44MP2j<#bb4n^5FX(_9IahtR|W0 zE}mG`LtB+5L6q9T?zVSc5$?reJ-o;j`S&!D>qqTfpPLuEt&-xpO=YJB)PsIQ6XQ9T z>CyJ0-*1jp>zTRNZWc`nJI?i>$KRcS-lXP`>7w;bmhQa_i+bTy(sDAdo9ETB9W=#P zip}?EZtOlo4rg=yQ7{I)5b0At{?G zm^yGXlGFL5FO@)@_PIRBsa(H~=S^|zR&c;K{q(0o@`d5_GLS>L#R{oDNhiP6TN~E7 zBGUtVUsV(i|1`N;enV&To5kzbbamitV3xN?$4JAbFK-YNK_#+(!HN5UXAotkaT<#OGTm{_Z}Pc73|T z!(Kei)XwEpd5&?}Y#q=M*75kL5<^<*t2(3cNdI`!_}|!RBiIU1o7)Sov8FPRj>;Qq z;xhkkCGH=F1%lLDG6pNoFwK>bjSMt-NS8Xr|hfraZ+vGsy?p-z;!mXqTV0 ze*v&w(-97DU>m%mw;U~VCI$VIyw=pxyr%3m!VOCPH2Rby;9nX@G{YxBn!|gxKMWd` zM&F%Ks*z3Br%gsDSI>@g`7o&4TuXq`Ny=scJdoD{)%K**v}nY%c%X^)R|EMZFV{N zPD`oIG$3uRUjdO3#I+!Zf12M}C|p1!r)C|Qtu9XE*hAkwzC)WN( zkZ^8-b6Zz0Ta2`0Xe<1__HhE2Lu2@v{BQZM500&UNBI$p2g&abIf4I2Wd+fM`vyJ&UzbXK7J$DB(EptXX-XjCiE1enP*UBpYo2N?aBz zqKSV3!ib>A^IdIGBD9H?bL)XZr2{?>5pQv`isg*Er2x1KetlIHv6SUXqa95i(rp? z1q}l1r_3Ij+phHv$27JiCkiLK2WB5k5PX}K*?&9R<7o(CW6{mxyb{Cjfp|{0 zY%H#-1I&;tcKCb*h=MM0+gHLMoObqF+xF_)?n7~79lQL@1qm;ZOybH7^1Pq0rdpdC zttr7wvkJfcXN^vP1%m_FKv2+xWSXxrk`<`s@^H3+i+hZiM@LdpyoGbh3kwgXHyAA| znS7Q&sLjq!az-v-UZP7$T`Cip+jKg6r~Nj?CBfzjxvY$jp@(1iW{-ZWs7!J#)^Hus z*3`8qdP?T$d66Ua+Q?rTnKZJRk#J?#Kck_q?q92!t=29*Lp0k+9nIMJ_=e?{!#D>$w@_}vqsO-YT!duLx; zZ2r)uh9Vm|R;}Pd)s$!X`}{VuX^ANz^$&0EuJUYO{l<8Lb!QLNZyJ(ZC|K?9`}dUu zZ0FUqjgl_m_)BnFMVXtmvIw$U+`W(|5fxwL?k&-E?Av@<78$1f-1%gnSQ)#FBg{7@ zJ8Y8Qd3~tlnNN;W8~OHr#w7{$o)Yl!<+p3)yShDE`&Lg&9_Vt{hr1U_b^0bY2Zo~1 zo?vhm@rk)z$mFKCPS~FNz-4@6z7 z1Pi@LzIyB}iEP~IkkSzZht*{Ev&j(39{s=!84Z@+eByh8*~LF!7#T;Zia+}6f1PDh zaz8sRhxt!z#Xzmx=m0TXnj9I<&jKXRiMNNxk3*@#PppTC14^8?ybpzwA!&IdvIYq2q%NVp z`1h#Q+FQR7*Rre(&VoH&b1K<1 zZHoLQ3EjxWAMf}jI)9=*36voQ;jfds5MZ-IXLG zF}=KlrDWov0;w&#pY#foE0P~W)nD`MWu59ycIy+JeY^VEt+WE~rmHcL%A!&oeV@wg z>{d_7hKYhLG~ROT)icgvsE3JdB?7&pBl*YbZRW7gmpNgdfj{m`6ZJMPJ{=G&_`LHw zwQ;@IFheEfvn>3nXKF6yh&@jY5bzY9OMjHpE5jLyx-BnbzBzjC!I8lDuNEg~fpSeGE@;1ZspI|u}Iw5LxPPZmllDL_;Quunh} z1j1%w_hl3hl9y#*^n`c05O0E`3YfWn$`y4LkgQ~+KD#q~F!Tcd0Y=Emt=BI>O<=g! zD{~p^A>0;GA3ZCV`^`XTqY0p~Hbfe-PxqD_`IV!Hv$$LTLg^?yBUzI~lxBrwdh#0K$4n{05uk#4$go;FFJ`w+Pa4lxL?7-n{!7-zlgl$M<_YbX`= zBbczT<4R9D#{Ym|`D-YxC4pRvb~g!u`mUGP|6R-U;4p-m!uTlhhi5Lc`>$@&sz^QTV+a1icsRUH zXVp~)PC{$p9$Ch(wDqsw<7XUvx|RF)+f7{9Ryo{QcN&LGQ__bc?<%s@6;dhv2xVSn zY{28{vE^y{<8v99pdD6E*dxh9jnyEBCV7O3dh2I#3lR3&*8Pcl&DSZ@6_z=+Fx=&LGN?QdP1s{E~;9RC!VCu_!0-G()$wih&pmP!7s%Lw}!3um$boC-S(?80obf2cAJd$q4s+-lpw=$6~tIt z>6Z8=lrcr^@Xp}$WtQ(SP7k`uIVqb@apzLyCtGIH&xogMJjA<%8 zkj-&O>&{}faHRD+-&+vrPcY1c8z;`Z<>Fd!u-(La{@H%CP3%N9LT5efsn#j=#Uru? zG0{}&Bg)?owpR5z>wkiLs|_)p4$g)t=x6RTwZ#uE4I-u#I}@9{f{bmvC_>eVNJ>le z9KRQp^{<1G`%zvhtUj41AJ%d#=Mo$;;a{f>x_XoB^K4c9P~{?+3W}}2Hb-I!rMb5m z14yi=4DkyQ(p~A8;8V^~%j~ zC&s{}n3k)J#`84jZsL+o{cA%*T{a8WYrvE2=AYN+-M?LyU1YHB-*r@9jj39#3Hw*K ztqaZnIAR~E__wG}ZZmh(8gpkc`tk?6KDUPL-L9qLEBDa8vlFljgCBKndQ}$w8}l{a ziSU_j$;osB*s7$Jag!8Cx!z+J5&XPNijm@k8g8WV7Y`XE!{$Vk6?ZsaAb`9VF0Gba z26lJ(ZHXhzW&z)Er!tc%*dpKkC)xE4t}_SLdm1C*EhDnE zXgj7uW`B3MYo%CPEQDeOk$beM(TU)l`QcnIc|EpF%2Ufl_3OGs&T?RTCXj=6Z&G+Sd$_#q z@|3?64oY_uNEqVm+~c~D<@Q$ZwCJ zdj{f<7O0EEVp`)#xmD}`Ao_|G{&%TVHt%Wf#OGdEG(?hHpPLc2F;R7(E_1!iqiiu)^umFVY65#BsqFa8O!u#YJ7*U|X^k6?)+DcfN+&+IlEi-YIoe!O zF)D5jtYs)%=-}siYB$jemd@b7KcR!jJwS_^E?P$ZaY4eKPt0EuL2B7u!5BxvTEtpscO(14d+1$#Ch zhnn+zoOPWUxL#osry|-wfmPXhT4G?C<$^}QmSuY7qaUvd%zGaU6@oba>8n~dRtLvK zm&e`nZ&?z~SOS;`0zXkHW0T4`tU|c4dInEOFh!6Fba`-rPoN9_+sLgOP8E8 zVY@krL*soWs{W@6vX1kUOlj;hgOry8qWnH?E^aPIyQ|MPFq@kt@K$Z3*%#smezX4) zuYC?*xgCw`HvjmcE;%^#`Nb^I$75W<#;Rc32eZ;QZekO>GruV@s_(H|yMs&Te*^3P zyu>{Uu;#8{F&EuGV%(WYv2RX`xq&snS@r%WFC4av`ZX6OhMeRt1=jgr9s+Z{R#*2G zxBhh18^JhP=kjV!3$&Y!x&mP(hF{))n=r69^0Cyz!g6`^@|oS=-;1q}ztlXlwY9Yy z`XqhfJ4UT$BkKi-@N?@Tjhwl#aeN4CJK`pjYO&6zSM5SRRau}_#sE&d7i0vyGk8?h zw3=*y2FTC70hH4N($wdcF`|GBs`cB@EEZixMPKii$-16~?t*{+&(-kB`VZE9sH)Y( zCXl+m=h0mpZ4wHQyNw+sqQh5!@6|6exxWubht48U|4~62ZZ4A!odZ}=ltUN&u1S_xbU2l zr#Ud@zwU;=)V;e}UNHWRw^sA5`Dde&*aXGuU)cn|cp6r0d5v=-B`WRg?cGX9z1S%SV$xi19HWB zetcO}rF@lSd$PakJEfKNsHwSm_BwA2n5Zn_;3)Hzy?sj>B1(d{-rsK)EB-6A%Lw7{ zIfK_cV!mh^e%ep@_&;xdfZ?m9e(mNDkOzBNu5?YJfpMa@*124x7?>Q{_Ee-O}l z_j`um-~&p5?=H^H2(TCx(a<1ZLZ!^O^D|bY@+|t@_1YPeox_b$%IedbTTJAF{=szKS95x}DQujc&MDg2*b3~pT_-oJ;O>VF<`sZSrV{;RYUee8CqaM5ixGJU4rKWTPx z_~*}`d#Gp{@#&4w4^q!@%k*kd8~#^G`m83~Sc+UiZ4obzc`7)A$~h$H6?nysg_O%AkBM4I~7>PEN0ntkM46 z+x>%CkzZ!rPa6)+ePr`+f17hG_2?@2UO2?<|Me1nk$PeT69a+L;FuKy=WZ#|oq>Y1F@A0eobN~DVfF*!q@PX33 zTN}pM+oLbc7oHyYf&K&|*`H+T{^d_bYQbI3?%I9gVvE!hO0=cdU6{UQ}?Z_BL zAbQBsHe(USVo5k$^J3&Jp-v#7F&O}&kQh1^*f5ppOSwu4iBki36Jji%$x;A=E334R zdmRFK>Cz?tTXO=;Od*pE`?L7E;oZ~CFrKf(f4Ne~$-E3@U*GBi>I@oXJ3BiXx-6@@ z!b@cPls-vz@8}inTWWn>K0ZF=XlN9ObXkbs?^(a};GQt7`CVj?T%S&;pnh;zZ-Wsm zOj|&Ws9C5%gbAqaKZse<%{QE7x34nA?AlTHDtyM(E$%ST#pB#waGORl-)WHTcMn$F z+J9_hWHg0q`u57qSUQ5CKk#4pQtFwrT^jdUxK&h{;(_||VhHWUp3OeF9tM`LMI#?x zkl(2xN8aQ1S`-9su%-dCvdUSOF@jLr zz2}h|mDGS-3iTIKJt5(nU)Ly0-TUw}?i1G<`i~@k_3lV{B&5+U!QZ*tk@*dQxLmqq z2q>vccq{aSkd)p}&;Rw*fAddH{xcX1yJDy>@poS(pmC)`8QGnUnUqvBj+v5AG@cZr zoW9M5S<7fZLYjy2Dd&VDYsQ@#t=!lH#ZKP-jy8gY7ypq&fvj>aH_sN!<+CY($u{gG zH*?=voej&qA3pG3BaQ#Ie|I1Y4H)h~+rir=w6pRyVHLL_e_QKE3f!{gRVG+~Rw>4? z%z8f}^Galc42y`C4CIfdv4&M|&j|`=b1}7v(_0pec}wDlIYW+vqDH!6;rrCl*P?hn zqtg--oVF~kij57c>vMI?(Z@l@`Lo$eKfE+E=G8H{@%NAXPu%nWy=3u|4QCU_$_o>N zTo{80higTQ*}E&k;*9MaC!d_t$Y~F91PAqOM}tcOdLQwKt_kaWaym;+;lJEHzuEr( zIHqZ86y_D}BqK4k@&xM>;$!pyOTCJ^geeNfh+-p}uZu;&lgs_`?$>BmMH8aTj5Bxs zAU^mvf48mp31CZ&jr-j1gU4=D+muhbb?^I(E+`4$yM3Q+ zUH^Y>#ec*PC0TOJuP(^_5@UHmlWPCywn!C;g7FpMO=DV;^chKp4puei8B`E5yw_&z z9|DfwzU1YN;#rHePl#Gqb+(fesl7q58{|}po=?zyuNG6O5rnh~x+D2-KQSZ?1EdD( z!$5JDZx{uHqkmaQ`HAy`tRH4`P8?R8zEJW=W4+LjRiy z|KpqfW;}8YC#hRG9bT50c_naCGXWXz>5OYqO8@4Q`^z+J5g?jYt=V6Y`nNwhSd5$w zi2ih5wESjW|9t(bymA6~tQULoziV25E$|hDgO6F%i!`d*t+vkmFRlC^KX;%OCGgt^ zc?66){}!ikvh&&ciiEw7F8}I={fFOiWsBl9O|xTbNpEg(;Q#2;3SW3Qs(}OXM_GY^ z!2e)xyUtUboHl$STKx8Z@aN=+)Mvx&lTq!l*Z$4(MYbT`tKflfdJ3}sUzyJT119BmKowWel z7`t(IPBCqgf~}fvg3|pT{N#Us`Q&_|!|97}Kgbz8o&v=XPN%r&fvb{_7Ga(fPK|a| z69brlWLBxC8~I0c>Q3N$nVoe%xk*F?~|Zlhvj(;a`RC^m|EC1&5%0;6yNXf_T2aky^(@LegX-Ub0%ewrK)wm&;^#ikKU|FD~ zXM6rc#Mq4?O|B)h3}E?%uZlR0E`UnYp*sA=BA%UvqLhI-X;%euNPF?_mt$IxlG3ru zB(1(Ey>B!0d&m0amYKnMBg@Sq)lM(g_Sg5>=Y1~`&OLkwJT1pc`1$=OH*?o?cT!WT zyQTLId_?D)_;y-@Nay3qyvtV5w8-k!uF36DWQ8v=EM5Fh*QA-&YgTg<6fZ&{Kh|_I zD8)Q4)c9)kRf4Rm+-Xb#W5h!7=sfeCCofpVU9$+hbpPT`eRmTqZ0kE|#8FIjC!g`p z>-5UcW6AY5)iS*m%WR{4Z&Du}>FB0t1y$-(#va$t(ct|-77S`+WmB1FCvI$y;(SI8fa{3qck%pv*y{SpRTsy1$ zCQ?f5mf*UXk>^bPir{tn+W5CrtmT3RZVteQKgrBn32JyB+n*I0BVB_c;bjZlykx7! z>El^!=!L#4QSGpi&iKtg92hV2zQxNs5rp()otH4s1(z-#A{muj#ydNxI&xXHW83ya zFO*03m$`Od?)J7PS!0}CLhEcqh}`*;4AI(8-P#j`@fb{t*Q3VV2zJ@=mm=@G9_;=8 z#+KuO+!y3zd=+XnQ(tbWba^!5EtS#F3gt$T3t{#_HOwiWmuE_C70dVApWBaJqVMy3 zRZ71q-Eiq+=dJSDUsrV^?Sm|`4XPeinrgCPir zN955rH5EC;2Fm51WQZ^V^^^i`jK||7vF>mdVVrKRMdj}e_e#`*D|+ZGMzEnb1=oLc zgh_p{;TI6cf=zu_WfSCYz}(?Ti9#Pb zox`FhxlzI&@1i-ptez*N((!Nysnp1NdKHTs;pr6LCn^PuG! z#iG+;9<+r2S`GgP7nHV5VSYu;-eRVHII%cn?!pyjrbZcGE6qq>yL*v|b)N=!;e`2q zFUQuf0k+1V(pTSu4l+cxcJHLUq$Rb{!uzJ@K2_X7L!-++{5dW&dg)LG7C~Il6OV8) zw)6kq2Y=?G89;woIP#Acr;X)Y>xISj*|x)|=p`bMMb0@tKr0z8GsmE@C3dM3!9t({R1Cy!>#i+n!Jok9-4l1e(Zc#jKBLDnmFc~dy_U5X&alnOtds84 zB9cOUiZznP`Yp3>m!K(tW@Q1P6F}AG2Qv2&`>S#P?-rf;YvExe?1@@DwsH_t!t2Ga-WokybcF__V$ zO7&gdZi`ZB>+kN>or8T76R2vFLyO~Q0iWb}!lWt!tY_*in7yN}%S-~RZ5_SWd5}MH zxHq}QGFgIFHLrO5T8#Y-p;hA4P6d-7F{Qi&gC_Eptt}drRcq5>nL;_Xk>fMd` z{gk_YhzrXCRr+OeVtJtDqCE!czOAOG`2Qnu|344nKfkj5^o+tq_Sdv}GUcT5V)yBN zcA}ctk3lVK)fK5QDKJ!Kbe97w{Uy<<5wswR+8EwcAEs|veah%DfEO@gl#%h1+FOL; zq88~rzrzrXu`Sic(Q>uno@BSfYdgJdhq=9%HajLsU+5z@>cw!)u&PMbohoGtq`C7n zb;~(Eh+^f$V-N$hmz2L78fXe-s@@_F>?<4MaugM=gj3*N<+Q#6*aI)XK%+K&lj`2~hIP z@^f$&=$AVs8&$bu?d|Q|a9Pu=NWxtdgk>RvV(p$aSB+#7mP?!4o!gqHz4kcy+ANb>xxzFhenR~>yF*X z=#PmbP|oh*HLfVM;&pX=n%zQ59N$D6hvp6mT^flgIo>NcUSAE@g8S|*VyZ#my$VQ& zjwideaye}+^^wRhw7RZkFzvcPs@-?0`^tX&tA%tS_1ArDsQQ>64I_|rGcmzyDs z=sKU7!bHT=DZhF~haK+joId{-$9=8+W2&BW9^?;HBoJ66`=tSz6nnP$#jaFj_U0W@mqu1YDwJ|7Dle>QkBK&*eQ6XldU^zeh_Xw#z zof{pJom?+qh-#n|ih>mbLGBjRk3ZROZCMp{?7Wmcs3%JV`&V>#qDXE&`nLxuAQx3v*XIk#8|6mnoQMz1mJ!K>dq#Qivk$%Ts zAeIo`y4buKZmzRGihi7iv(PB7K7Ku{7s_*!!Nzi)O?Y^vwCxsq7{SG*>c{1p1*Z+h zDpg+rHRro94sB4qFDb~;`I^bt$@L?ae-tg3MEPSqzq*SE&C(XbC+XT%+BtxC~P0g3Sz?cVXg9$(~wn?a^zSE)wEO1 zmhKMl2BR;l_^TGK?Q|X=bQ*k>udfK$!vwgpAYdy!D*PURH^X|{!>&401C+t1TOyqY zcRBYij=eHV>L|cn$#Gi-aF1P_S22!_49WKwhZx2RT4Hc7I#Q%Z=r87LxO1&k5%F$q zvAjH1x|rF31qJr?4P5H+Pxj+UHR&y#a&YC@zzcR-MQeZ3?ev$|mQL8{c!u1l%jABy zH~mNO6hlNNxj*+$oh665+4ktoF+0&zZ9;^J+d=v0WkKtH_eEG#pud$=n-3e ze3xr$J_|SkU%l`_0S(;xWPcA4VFT_{CMU=2>q_|qNe9~!LI?Xi4>r!5(jIXX2Nw2; zo=4g<@eVVlYq-32D?jLp1QRhS5A9YN^au#)s?-gf4RezbxW!%y`hXJl{q(pq^ckt0 z;ndBS+`H}#y$Mp6*|qdsm{S(58wX7(%<+0$U3BkQr~h^_$#-HuCDjXPwe2)|@noR@ zfWit6zr~C(_kyB({RvS`?SnqPf-#YB-*ztm4qCJdc;L|Ni0=HS8xG2w#iy4yN}n(F z%u?}xD&`|%2b7t;UEcaG&5Ck2e?#rXI+BQ~tqiYUnUi<_&R@Hq`St|t4>?uJsO1FP z+THjRW8mTyx7(#7om~R_)3>1`B3>450wBYGTw3v69qt@SG#;5ga&}UGYpQ$Xo}Dh1 zkBq!DJXYBf=A{rqi$#d_*$SvlW?4gOp#*1l0X~)&)cdTU6&fN5FA@XqL0B4q3^|1Nwb|1t!rHB-EOLB296#|uYE4egP4=k3_W{*VxFXz z3E`-E%H{|NH{b6Mb^6cUtXoB`x&ZdD4IpApb z&mbBWz_r5av{oSd^UH<9E?e^LxpWy!reJ==aR0%jgP!cl!?igF>>GK3TbUd>RRff3 zSDmgJqmbnOa`6}#mOpYxHT9l_?zB7m6H;|N;OV#N>U}o5A5;V2)41MTTZi#fvb?zn z?=@P*85}iJ{z)b}*o(1O9jr1;HSkWCZj8(p*G?n*QJ2>w`_Ux(DDZg4_p9Hd4z|nd z#;`$e6Sj9{kEUh!_J`SrR}mn5crUw6j)^M4+)fKNBQizrN7FD_AvEBV6o@1Isl{uo zK~&`b;j67MZ_QjAkfKclkfngih&o%jpK)~uR%J_35f?8>BFyW?0gKMAg|T@Hi0UWw zgoXk9ZLE6Y&}j!qkC)D*vu4(dli6RMZoK4~$1~%)kp9y8IV;3x z5J{3u*nI$JQQOV)1 z#wwO9GEM({sJq*|c+Uv-qNMIe1sMHL3i$>$?LZygM*@=L;rk#~+V|fW*b5AKrxUQ5 zVrTh7#ODmX?4Jn7v{|Ac>C;BX#tAM;N@#QszW`)VZ%BWc^w_&WWZgc<^w z&o$s96mDiL6F--zCV>AcQ$00cZ#>^Te3FonJMI@~2%EkOoxa;xYLv*eIV3zT)L|`WC zG*08fDbWTdwG^qLG1oR;HFF93IAhmGB4O?`Zy!_vD*6U8)5x~_wr>5I;BD=I)IT{AaAIu%z3 z1dU7PQ+%s{Ap3W($%*5`i7_(m%8J_dPoOm}im^~pZ3%?&(KbE-QI(64Ls76VI1R-G z01c%e!i+gPtjJ^%jpF;1IV~Tk7QEE34P?*8J za)P`N5;jC{dzR*p`q})6qWBSzWbH?}XYyDpc1~`cX&w^Dl8`x?g&#HX>KA^dVuV`e zm7EL9)xrB0XqQS-UWM(drREF$Hm2UA(GRXL44Go^+uuCVA{M;5URt6P4&rM+U$NS^ zRBrj{z`45SKOg`g+k3m(#go%-mW!Ros||9_kpf>hvutjE0CthU*_&EQ% zvkCIlI0+8zU|&XFLr|Fb%CUP_b@CErzP(tfikbv_T`kB3xG5#I_ zIksJp3bx7*9?3Dog=pNyz%E3D@-SeI`oZnoaue@O1g@k>6{b-QV*FxrwPXbbSAwrD ztSJxZY}*aB8f&8;|Li;M;fGx}Qb&KpB?Nsphc*a+WfYB6uhx!`(}2R49+fbd&=%|j z34zOmjSMNBns%r_pcw6s7o|}Ew_5d_US{y6L#SgS_YoG6rVOJ8sKP~KGVA8A>4hkT zGe7y^)t>pFTx?mV$aTdIYPyxJ7bE16$Mpfz8dz-8xovI*gWk>_;y0ev4bz6hu4<;v zZcED%+hBJ{A1oq%&Km_kfO_^<_Hw_*y~%69m=nC1V4wTEPQi9{phlpxPHp0?!+;)T>VcZJ)BWJcTEP9dPmGxS z`mE{*zx)c)vrwfxDCF7;*rMizAW8Uc`%N~J7OnI%y!<{WKEHg3+`{m8z*RRdNY~1^ z49v(sTES%XTVE_coP__ht}JTbe;a<~VJ0K)RR@p)Bg$;VcW$!2));8K?6YuH_F57? zZWh74oN0elrIzbRpNP+2WJyq()t*zzY_l72N7*MjmYudy-2^}aMNjpy=SUVMVeLBr zso1jR?(7~ZJ)l?k5>mm%tcS?qjZj9KzjP9}$*YsQWFz2IKIYlR%GTB75Zv4AenKCH z>t&GBdf1S_{oL-Crm)EZj8aTE`IY;}k7kh}@_mI-2#KOaOH8XF!bi|(wm^Ux<9G-z z>RA~eT)e-_ASE&N>iU6@>Z@%<=exN$VZf`myykz@6d&N;qZOsbyJtw(XV;jIsDZq@ z^+gYndxM#Y?gCW8JXdyD^btT4Ml+uQK`+U;oZLKWjZ5ZdVe}mt8K29ou=}Nj>bkX% ze${)+Jbn6JU>kXYcIe7;#w_}<0>4=v|9ISeOyTn_UfOV&;T<&^-9Em3vjGdrM+_w; zI1OA{4*iNeZ&f~o(*(@t4#gd;9v>CZUmfy&7%6O=ixcGz!_oVf3cSea3JFq%oZ|YC zKKY*K^aGvq5uuk`hUl0|$1_vAKrnS2{< zVdZ4jP0Bm8Vg1GQhI*S)PV*r?n+mDl_8fEzRrX=~TB$n{Av2j>7I?JG(#9_kFkfCP zG0oM;YKTB|gM71iFRx?kNP|-srDQvXn=&ApCCNYC>quX7XO?M$h7qC~mh|vw^GVo^ z(S$OCNm0}(?BZbA=N!LH>1Lexusy@=UUB1zNQNKfh|;}JU0%5ri4iEr$cRJhJx(qs zs#r1(??0QB|K%$o`LywLv5{u^$%+fx6MlzwJ1d?ZkX~U6a5EhE^5Wl8W!mS=7`}(t z>(|plIkW<^YzbS<;%;*l`~kadGNBoo$2xge^|WQ=?g`pgox$77RW;+$_JTDv&J1-u zUkzCv2@JfFNd5KY=11|^1$z(P2a4}AGR+o!>hSXmKGg}Xr#;6^BV$mSU)v!bqkbFh zY6I>bH9DF5wnv57f>yqqt}kA7`zCPr`V_r-s@XsCDs-OGDF>@X*LQCVD_m#Rv*hnv zeSwxUMho5(z)Nc1AvNjkk1lOhO4_B1`IhO6@qyvq507PtRf4BZP4}iQG=@$sXJIJU$vH3YCZj%>Df3$ zWr9raZOaQGO$BHS75XusU50Q!HfG+d?>&Nq+`5v%_&`979SwmfCPo~B zpsC#7m1{E6#R#ZE|Eb0P!<^t!B9cTT=i=<7-VNnX_SELWbg+ZuRlk;rfjf?kjeu*r z7S!)Z8w8$ROp=T7rWoE5)jjaVV_8H&fUjz$yJhlhLn)*=7%sKNMAuzs*@Z;!q!==b zL7pS`Zs%C%bS!x4t&8EO(eFH77QFVq5$)4)CQkDk3Z=#)Rt=hta83a@26hC31Q&=w{KxOuN>ysU8Y=ue(8tL-}(dh ztjxcNushIEKgoV#!2^IxQItF2a6yfOFRVmTZW<@?a;*l6mA|(7UGb&;N@3@~8JwMH zqcU>xjWXTL&{}RgV^pqhAZOgS_m)RzYhsix(?(*H3xr|k!w6OGmvjb1gecMy-U!{2 z!+ahBaM42{XoRMV>OCQ-beD"KrLEvQ9Lsfr0J!8QM3AA-G77|MBAHJ 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 literal 0 HcmV?d00001 diff --git a/tools/target-server-validator/input.properties b/tools/target-server-validator/input.properties index feb069ca..5a2907fc 100644 --- a/tools/target-server-validator/input.properties +++ b/tools/target-server-validator/input.properties @@ -27,9 +27,14 @@ allow_insecure=false [gcp_metrics] enable_gcp_metrics=true -project_id=xxx-xxx-xxx -metric_name=custom.googleapis.com/host_status_v10 +project_id=xx-xxx-xxx +metric_name=custom.googleapis.com/host_status enable_dashboard=true -dashboard_title=Apigee Target Server Health Monitoring Dashboard v3 -alert_policy_name=Apigee Target Server Validator Policy v3 -notification_channel_id=xxxx \ No newline at end of file +dashboard_title=Apigee Target Server Health Monitoring Dashboard +alert_policy_name=Apigee Target Server Validator Policy +notification_channel_id=xxxxx + +[gcs_bucket] +bucket_name=target-server-validator-gcs +bucket_project_id=xxx-xxx-xxx +file_path_in_bucket=scan_output.txt \ No newline at end of file diff --git a/tools/target-server-validator/main.py b/tools/target-server-validator/main.py index 261aed8c..65ef61ea 100644 --- a/tools/target-server-validator/main.py +++ b/tools/target-server-validator/main.py @@ -18,7 +18,7 @@ import os import sys import json -import time +import argparse from utilities import ( # pylint: disable=import-error parse_config, create_proxy_bundle, @@ -33,12 +33,25 @@ create_custom_metric, report_metric, create_custom_dashboard, + get_metric_descriptor, + gcs_upload_json, + download_json_from_gcs, ) from apigee_utils import Apigee # pylint: disable=import-error from base_logger import logger def main(): + + # Arguments + parser = argparse.ArgumentParser(description='details', + usage='use "%(prog)s --help" for more information',formatter_class=argparse.RawTextHelpFormatter) # noqa + parser.add_argument('--onboard', action='store_true', help='Toggle to onboard validator proxy, custom metric descriptors and dashboard') # noqa + parser.add_argument('--scan', action='store_true', help='Toggle to read all resources') # noqa + parser.add_argument('--monitor', action='store_true', help='Toggle to check the status of target servers and push to GCP Logging') # noqa + + args = parser.parse_args() + # Parse Inputs cfg = parse_config("input.properties") check_proxies = cfg["validation"].getboolean("check_proxies") @@ -46,6 +59,15 @@ def main(): enable_gcp_metrics = cfg["gcp_metrics"].getboolean("enable_gcp_metrics") report_format = cfg["validation"]["report_format"] allow_insecure = cfg["validation"].getboolean("allow_insecure") + project_id = cfg["gcp_metrics"]["project_id"] + metric_name = cfg["gcp_metrics"]["metric_name"] + enable_dashboard = cfg["gcp_metrics"]["enable_dashboard"] + vhost_domain_name = cfg["validation"]["api_hostname"] + vhost_ip = cfg["validation"].get("api_ip", "").strip() + api_url = f"https://{vhost_domain_name}/validate-target-server" + bucket_name = cfg["gcs_bucket"]["bucket_name"] + file_path_in_bucket = cfg["gcs_bucket"]["file_path_in_bucket"] + bucket_project_id = cfg["gcs_bucket"]["bucket_project_id"] if report_format not in ["csv", "md"]: report_format = "md" @@ -64,210 +86,233 @@ def main(): cfg["target"]["org"], ) - environments = source_apigee.list_environments() - all_target_servers = [] - - # Fetch Target Servers from Source Apigee@ - logger.info("exporting Target Servers !") - for each_env in environments: - target_servers = source_apigee.list_target_servers(each_env) - args = ((each_env, each_ts) for each_ts in target_servers) - results = run_parallel(source_apigee.fetch_env_target_servers_parallel, args) # noqa - for result in results: - _, ts_info = result - ts_info["env"] = each_env - ts_info["extracted_from"] = "TargetServer" - all_target_servers.append(ts_info) - - # Fetch Targets in APIs & Shared Flows from Source Apigee - proxy_hosts = {} - proxy_targets = {} - if check_proxies: - skip_proxy_list = ( - cfg["validation"].get("skip_proxy_list", "").split(",") - ) - logger.info("exporting proxies to be analyzed ! this may take a while !") # noqa - api_types = ["apis", "sharedflows"] - api_revision_map = {} - for each_api_type in api_types: - api_revision_map[each_api_type] = {} - api_revision_map[each_api_type]["proxies"] = {} - api_revision_map[each_api_type]["export_dir"] = ( - proxy_export_dir + f"/{each_api_type}" + if args.onboard: + # Create Validation Proxy Bundle + bundle_path = os.path.dirname(os.path.abspath(__file__)) + logger.info("Creating proxy bundle !") + create_proxy_bundle(bundle_path, cfg["validation"]["api_name"], "apiproxy") # noqa + + # Deploy Validation Proxy Bundle + logger.info("Deploying proxy bundle !") + if not target_apigee.deploy_api_bundle( + cfg["validation"]["api_env"], + cfg["validation"]["api_name"], + f"{bundle_path}/{cfg['validation']['api_name']}.zip", + cfg["validation"].getboolean("api_force_redeploy", False) + ): + logger.error(f"Proxy: {cfg['validation']['api_name']} deployment failed.") # noqa + sys.exit(1) + # CleanUp Validation Proxy Bundle + logger.info("Cleaning Up local proxy bundle !") # noqa + delete_file(f"{bundle_path}/{cfg['validation']['api_name']}.zip") + + # create metric descriptor and dashboard + if enable_gcp_metrics: + logger.info("Creating metric descriptor") + descriptor = create_custom_metric(project_id, metric_name) + # report_metric(project_id, descriptor, final_report) + if enable_dashboard: + logger.info(f"Creating dashboard in project {project_id}") + dashboard_title = cfg["gcp_metrics"]["dashboard_title"] + alert_policy_name = cfg["gcp_metrics"]["alert_policy_name"] + notification_channel_id = cfg["gcp_metrics"]["notification_channel_id"] # noqa + create_custom_dashboard(project_id, dashboard_title, metric_name, alert_policy_name, notification_channel_id) # noqa + + if args.scan: + environments = source_apigee.list_environments() + all_target_servers = [] + + # Fetch Target Servers from Source Apigee@ + logger.info("exporting Target Servers !") + for each_env in environments: + target_servers = source_apigee.list_target_servers(each_env) + target_server_args = ((each_env, each_ts) for each_ts in target_servers) # noqa + results = run_parallel(source_apigee.fetch_env_target_servers_parallel, target_server_args) # noqa + for result in results: + _, ts_info = result + ts_info["env"] = each_env + ts_info["extracted_from"] = "TargetServer" + all_target_servers.append(ts_info) + + # Fetch Targets in APIs & Shared Flows from Source Apigee + proxy_hosts = {} + proxy_targets = {} + if check_proxies: + skip_proxy_list = ( + cfg["validation"].get("skip_proxy_list", "").split(",") + ) + logger.info("exporting proxies to be analyzed ! this may take a while !") # noqa + api_types = ["apis", "sharedflows"] + api_revision_map = {} + for each_api_type in api_types: + api_revision_map[each_api_type] = {} + api_revision_map[each_api_type]["proxies"] = {} + api_revision_map[each_api_type]["export_dir"] = ( + proxy_export_dir + f"/{each_api_type}" + ) + create_dir(proxy_export_dir + f"/{each_api_type}") + + for each_api in source_apigee.list_apis(each_api_type): + if each_api not in skip_proxy_list: + api_revision_map[each_api_type]["proxies"][ + each_api + ] = source_apigee.list_api_revisions(each_api_type, each_api)[ # noqa + -1 + ] + else: + logger.info(f"Skipping API {each_api}") + + parallel_args = ( + ( + each_api_type, + each_api, + each_api_rev, + each_api_type_data["export_dir"] + ) + for each_api_type, each_api_type_data in api_revision_map.items() # noqa + for each_api, each_api_rev in each_api_type_data["proxies"].items() # noqa ) - create_dir(proxy_export_dir + f"/{each_api_type}") - - for each_api in source_apigee.list_apis(each_api_type): - if each_api not in skip_proxy_list: - api_revision_map[each_api_type]["proxies"][ - each_api - ] = source_apigee.list_api_revisions(each_api_type, each_api)[ # noqa - -1 - ] + logger.debug("Exporting proxy target servers") + results = run_parallel(source_apigee.fetch_api_proxy_ts_parallel, parallel_args) # noqa + + for result in results: + each_api_type, each_api, parsed_proxy_hosts, proxy_ts = result + if proxy_hosts.get(each_api_type): + proxy_hosts[each_api_type][each_api] = parsed_proxy_hosts else: - logger.info(f"Skipping API {each_api}") + proxy_hosts[each_api_type] = {} + proxy_hosts[each_api_type][each_api] = parsed_proxy_hosts + + for each_te in proxy_ts: + if each_te in proxy_targets: + proxy_targets[each_te].append( + f"{each_api_type} - {each_api}" + ) + else: + proxy_targets[each_te] = [ + f"{each_api_type} - {each_api}" + ] + logger.debug("Exporting proxy target servers done") + + # Fetch API Northbound Endpoint + logger.info(f"Fetching VHost with name {cfg['validation']['api_hostname']} !") # noqa + + for each_api_type, apis in proxy_hosts.items(): + for each_api, each_targets in apis.items(): + for each_target in each_targets: + if ( + not has_templating(each_target["host"]) + and not each_target["target_server"] + ): + each_target["env"] = "_ORG_API_" + if each_api_type == "apis": + each_target["extracted_from"] = "APIProxy" + else: + each_target["extracted_from"] = "SharedFlow" + each_target["name"] = each_api + each_target["info"] = each_target["source"] + all_target_servers.append(each_target) + + if cfg["validation"].getboolean("check_csv"): + csv_file = cfg["csv"]["file"] + default_port = cfg["csv"]["default_port"] + csv_rows = read_csv(csv_file) + for each_row in csv_rows: + each_host, each_port = get_row_host_port(each_row, default_port) # noqa + ts_csv_info = {} + ts_csv_info["host"] = each_host + ts_csv_info["port"] = each_port + ts_csv_info["name"] = each_host + ts_csv_info["info"] = "_NA_" + ts_csv_info["env"] = "_NA_" + ts_csv_info["extracted_from"] = "Input CSV" + all_target_servers.append(ts_csv_info) - args = ( + scan_output = { + "all_target_servers": all_target_servers, + "proxy_targets": proxy_targets + } + + # upload output to gcs + gcs_upload_json(bucket_project_id, bucket_name, file_path_in_bucket, scan_output) # noqa + + if args.monitor: + # Download scan ouput from gcs + scan_output = download_json_from_gcs(bucket_project_id, bucket_name, file_path_in_bucket) # noqa + + if not scan_output: + return + + all_target_servers = scan_output["all_target_servers"] + proxy_targets = scan_output["proxy_targets"] + + batch_size = 5 + batches = [] + new_structure = [] + + for entry in all_target_servers: + host = entry.get('host', '') + port = entry.get('port', '') + + if host and port: + new_entry = { + 'host': host, + 'port': str(port), + 'name': entry.get('name', ''), + 'env': entry.get('env', ''), + 'extracted_from': entry.get('extracted_from', ''), + 'info': entry.get('info', '') + } + + new_structure.append(new_entry) + + if len(new_structure) == batch_size: + batches.append(new_structure) + new_structure = [] + + if new_structure: + batches.append(new_structure) + + validator_args = ( ( - each_api_type, - each_api, - each_api_rev, - each_api_type_data["export_dir"] + api_url, + vhost_domain_name, + vhost_ip, + json.dumps(batch), + allow_insecure, + proxy_targets ) - for each_api_type, each_api_type_data in api_revision_map.items() - for each_api, each_api_rev in each_api_type_data["proxies"].items() + for batch in batches ) - logger.debug("Exporting proxy target servers") - results = run_parallel(source_apigee.fetch_api_proxy_ts_parallel, args) - for result in results: - each_api_type, each_api, parsed_proxy_hosts, proxy_ts = result - if proxy_hosts.get(each_api_type): - proxy_hosts[each_api_type][each_api] = parsed_proxy_hosts + output_reports = run_parallel(source_apigee.call_validator_proxy_parallel, validator_args) # noqa + final_report = [] + for output in output_reports: + if isinstance(output, list): + final_report.extend(output) else: - proxy_hosts[each_api_type] = {} - proxy_hosts[each_api_type][each_api] = parsed_proxy_hosts - - for each_te in proxy_ts: - if each_te in proxy_targets: - proxy_targets[each_te].append( - f"{each_api_type} - {each_api}" - ) - else: - proxy_targets[each_te] = [ - f"{each_api_type} - {each_api}" - ] - logger.debug("Exporting proxy target servers done") - - bundle_path = os.path.dirname(os.path.abspath(__file__)) - - # Create Validation Proxy Bundle - logger.info("Creating proxy bundle !") - create_proxy_bundle(bundle_path, cfg["validation"]["api_name"], "apiproxy") - - # Deploy Validation Proxy Bundle - logger.info("Deploying proxy bundle !") - if not target_apigee.deploy_api_bundle( - cfg["validation"]["api_env"], - cfg["validation"]["api_name"], - f"{bundle_path}/{cfg['validation']['api_name']}.zip", - cfg["validation"].getboolean("api_force_redeploy", False) - ): - logger.error(f"Proxy: {cfg['validation']['api_name']} deployment failed.") # noqa - sys.exit(1) - # CleanUp Validation Proxy Bundle - logger.info("Cleaning Up local proxy bundle !") # noqa - delete_file(f"{bundle_path}/{cfg['validation']['api_name']}.zip") - - # Fetch API Northbound Endpoint - logger.info(f"Fetching VHost with name {cfg['validation']['api_hostname']} !") # noqa - vhost_domain_name = cfg["validation"]["api_hostname"] - vhost_ip = cfg["validation"].get("api_ip", "").strip() - api_url = f"https://{vhost_domain_name}/validate-target-server" - final_report = [] - - for each_api_type, apis in proxy_hosts.items(): - for each_api, each_targets in apis.items(): - for each_target in each_targets: - if ( - not has_templating(each_target["host"]) - and not each_target["target_server"] - ): - each_target["env"] = "_ORG_API_" - if each_api_type == "apis": - each_target["extracted_from"] = "APIProxy" - else: - each_target["extracted_from"] = "SharedFlow" - each_target["name"] = each_api - each_target["info"] = each_target["source"] - all_target_servers.append(each_target) - - if cfg["validation"].getboolean("check_csv"): - csv_file = cfg["csv"]["file"] - default_port = cfg["csv"]["default_port"] - csv_rows = read_csv(csv_file) - for each_row in csv_rows: - each_host, each_port = get_row_host_port(each_row, default_port) - ts_csv_info = {} - ts_csv_info["host"] = each_host - ts_csv_info["port"] = each_port - ts_csv_info["name"] = each_host - ts_csv_info["info"] = "_NA_" - ts_csv_info["env"] = "_NA_" - ts_csv_info["extracted_from"] = "Input CSV" - all_target_servers.append(ts_csv_info) - - batch_size = 5 - batches = [] - new_structure = [] - - for entry in all_target_servers: - host = entry.get('host', '') - port = entry.get('port', '') - - if host and port: - new_entry = { - 'host': host, - 'port': str(port), - 'name': entry.get('name', ''), - 'env': entry.get('env', ''), - 'extracted_from': entry.get('extracted_from', ''), - 'info': entry.get('info', '') - } - - new_structure.append(new_entry) - - if len(new_structure) == batch_size: - batches.append(new_structure) - new_structure = [] - - if new_structure: - batches.append(new_structure) - - args = ( - ( - api_url, - vhost_domain_name, - vhost_ip, - json.dumps(batch), - allow_insecure, - proxy_targets - ) - for batch in batches - ) + logger.error(output.get("error", "Unknown Error occured while calling proxy")) # noqa + + if enable_gcp_metrics: + logger.info("Dumping data to stack driver") + # get metric descriptor + descriptor = get_metric_descriptor(project_id, metric_name) + if descriptor: + report_metric(project_id, descriptor, final_report) + else: + logger.error("Couldn't push data to stackdriver because the the existing metric descriptor couldn't be fetched.") # noqa + + elif report_format == "md": + report_file = "report.md" + logger.info(f"Dumping report to file {report_file}") + write_md_report(report_file, final_report) - output_reports = run_parallel(source_apigee.call_validator_proxy_parallel, args) # noqa - for output in output_reports: - if isinstance(output, list): - final_report.extend(output) - else: - logger.error(output.get("error", "Unknown Error occured while calling proxy")) # noqa - - if enable_gcp_metrics: - project_id = cfg["gcp_metrics"]["project_id"] - metric_name = cfg["gcp_metrics"]["metric_name"] - enable_dashboard = cfg["gcp_metrics"]["enable_dashboard"] - logger.info("Dumping data to stack driver") - descriptor = create_custom_metric(project_id, metric_name) - report_metric(project_id, descriptor, final_report) - if enable_dashboard: - logger.info(f"Creating dashboard in project {project_id}") - dashboard_title=cfg["gcp_metrics"]["dashboard_title"] - alert_policy_name=cfg["gcp_metrics"]["alert_policy_name"] - notification_channel_id = cfg["gcp_metrics"]["notification_channel_id"] - create_custom_dashboard(project_id, dashboard_title, metric_name, alert_policy_name, notification_channel_id) - - elif report_format == "md": - report_file = "report.md" - logger.info(f"Dumping report to file {report_file}") - write_md_report(report_file, final_report) - - # Write CSV Report - # TODO: support relative report path - elif report_format == "csv": - report_file = "report.csv" - logger.info(f"Dumping report to file {report_file}") - write_csv_report(report_file, final_report) + # Write CSV Report + # TODO: support relative report path + elif report_format == "csv": + report_file = "report.csv" + logger.info(f"Dumping report to file {report_file}") + write_csv_report(report_file, final_report) if __name__ == "__main__": diff --git a/tools/target-server-validator/requirements.txt b/tools/target-server-validator/requirements.txt index 4143da31..55bfd5b2 100644 --- a/tools/target-server-validator/requirements.txt +++ b/tools/target-server-validator/requirements.txt @@ -20,3 +20,4 @@ requests==2.31.0 forcediphttpsadapter==1.0.2 google-cloud-monitoring==2.19.1 google-cloud-monitoring-dashboards==2.14.1 +google-cloud-storage==2.14.0 diff --git a/tools/target-server-validator/utilities.py b/tools/target-server-validator/utilities.py index 17238ec3..b9ec2fcd 100644 --- a/tools/target-server-validator/utilities.py +++ b/tools/target-server-validator/utilities.py @@ -17,6 +17,7 @@ import os import sys +import json import configparser import zipfile import csv @@ -26,8 +27,8 @@ from google.cloud import monitoring_v3 from google.api import metric_pb2 as ga_metric from google.cloud import monitoring_dashboard_v1 -from google.cloud import monitoring_v3 from google.protobuf import duration_pb2 +from google.cloud import storage import requests import xmltodict import urllib3 @@ -277,9 +278,20 @@ def run_parallel(func, args, workers=10): return data +def get_metric_descriptor(project_id, metric_name): + descriptor_name = f"projects/{project_id}/metricDescriptors/{metric_name}" + + client = monitoring_v3.MetricServiceClient() + try: + descriptor = client.get_metric_descriptor(name=descriptor_name) + return descriptor + except Exception as e: + logger.error(f"Error while getting the existing metric descriptor. ERROR-INFO: {e}") # noqa + return None + + def create_custom_metric(project_id, metric_name): client = monitoring_v3.MetricServiceClient() - project_name = f"projects/{project_id}" # Create metric descriptor descriptor = ga_metric.MetricDescriptor() @@ -291,12 +303,13 @@ def create_custom_metric(project_id, metric_name): ga_label.LabelDescriptor(key='status', value_type='STRING') ]) try: - descriptor = client.create_metric_descriptor(name=project_name, metric_descriptor=descriptor) # noqa + descriptor = client.create_metric_descriptor(name=f"projects/{project_id}", metric_descriptor=descriptor) # noqa return descriptor except Exception as e: logger.error(f"Error while creating the metric descriptor. ERROR-INFO: {e}") # noqa return None + def get_status_int(status): if status == "REACHABLE": return 1 @@ -305,9 +318,9 @@ def get_status_int(status): elif status == "UNKNOWN_HOST": return 0 + def report_metric(project_id, metric_descriptor, sample_data): client = monitoring_v3.MetricServiceClient() - project_name = f"projects/{project_id}" series = monitoring_v3.TimeSeries() @@ -331,28 +344,29 @@ def report_metric(project_id, metric_descriptor, sample_data): series.metric.labels['status'] = data[5] series.points = [point] - client.create_time_series(name=project_name, time_series=[series]) + client.create_time_series(name=f"projects/{project_id}", time_series=[series]) # noqa logger.debug(f"Pushed to stackdriver - {data[2]} {data[5]}") logger.info("Successfully pushed data to stackdriver") except Exception as e: logger.error(f"Error while pushing the data to stackdriver. ERROR-INFO: {e}") # noqa -def create_alert_policy(project_name, policy_name, metric_name, notification_channel_id): +def create_alert_policy(project_id, policy_name, metric_name, notification_channel_id): # noqa client = monitoring_v3.AlertPolicyServiceClient() conditions = [ monitoring_v3.AlertPolicy.Condition( display_name="Target Server Validator Policy", - condition_threshold=monitoring_v3.AlertPolicy.Condition.MetricThreshold( - filter=f"resource.type = \"global\" AND metric.type = \"{metric_name}\"", + condition_threshold=monitoring_v3.AlertPolicy.Condition.MetricThreshold( # noqa + filter=f"resource.type = \"global\" AND metric.type = \"{metric_name}\"", # noqa comparison=monitoring_v3.ComparisonType.COMPARISON_LT, threshold_value=0.75, - duration=duration_pb2.Duration(seconds=0), + duration=duration_pb2.Duration(seconds=60), aggregations=[ monitoring_v3.Aggregation( - alignment_period=duration_pb2.Duration(seconds=300), - per_series_aligner=monitoring_v3.Aggregation.Aligner.ALIGN_NEXT_OLDER, - cross_series_reducer=monitoring_v3.Aggregation.Reducer.REDUCE_NONE, + alignment_period=duration_pb2.Duration(seconds=120), + per_series_aligner=monitoring_v3.Aggregation.Aligner.ALIGN_NEXT_OLDER, # noqa + cross_series_reducer=monitoring_v3.Aggregation.Reducer.REDUCE_NONE, # noqa + group_by_fields=["metric.label.hostname"], ) ], trigger=monitoring_v3.AlertPolicy.Condition.Trigger( @@ -362,7 +376,7 @@ def create_alert_policy(project_name, policy_name, metric_name, notification_cha ) ] - notification_channels = [f"projects/apigee-hybrid-testing-415007/notificationChannels/{notification_channel_id}"] + notification_channels = [f"projects/{project_id}/notificationChannels/{notification_channel_id}"] # noqa policy = monitoring_v3.AlertPolicy( display_name=policy_name, conditions=conditions, @@ -371,21 +385,21 @@ def create_alert_policy(project_name, policy_name, metric_name, notification_cha ) created_policy = client.create_alert_policy( - name=project_name, + name=f"projects/{project_id}", alert_policy=policy ) logger.info(f"Created alert policy: {created_policy.name}") return created_policy.name -def create_custom_dashboard(project_id, dashboard_title, metric_name, policy_name, notification_channel_id): +def create_custom_dashboard(project_id, dashboard_title, metric_name, policy_name, notification_channel_id): # noqa client = monitoring_dashboard_v1.DashboardsServiceClient() - request = monitoring_dashboard_v1.ListDashboardsRequest(parent=f"projects/{project_id}") - + request = monitoring_dashboard_v1.ListDashboardsRequest(parent=f"projects/{project_id}") # noqa + existing_dashboards = client.list_dashboards(request=request).dashboards for dashboard in existing_dashboards: if dashboard.display_name == dashboard_title: - logger.info(f"Dashboard '{dashboard_title}' already exists. Skipping creation.") + logger.info(f"Dashboard '{dashboard_title}' already exists. Skipping creation.") # noqa return dashboard = monitoring_dashboard_v1.Dashboard() @@ -395,11 +409,10 @@ def create_custom_dashboard(project_id, dashboard_title, metric_name, policy_nam ) dashboard.grid_layout = grid_layout - #create alerting policy - project_name=f"projects/{project_id}" - alert_policy_name = create_alert_policy(project_name, policy_name, metric_name, notification_channel_id) + # create alerting policy + alert_policy_name = create_alert_policy(project_id, policy_name, metric_name, notification_channel_id) # noqa widget = monitoring_dashboard_v1.Widget() - widget.alert_chart = monitoring_dashboard_v1.AlertChart(name=alert_policy_name) + widget.alert_chart = monitoring_dashboard_v1.AlertChart(name=alert_policy_name) # noqa dashboard.grid_layout.widgets.append(widget) request = monitoring_dashboard_v1.CreateDashboardRequest( @@ -407,4 +420,28 @@ def create_custom_dashboard(project_id, dashboard_title, metric_name, policy_nam dashboard=dashboard, ) response = client.create_dashboard(request=request) - logger.info(f"Dashboard created: {response.name}") \ No newline at end of file + logger.info(f"Dashboard created: {response.name}") + + +def gcs_upload_json(project_id, bucket_name, destination_blob_name, json_data): + try: + storage_client = storage.Client(project=project_id) + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(destination_blob_name) + blob.upload_from_string(json.dumps(json_data)) + logger.info(f'Scan output uploaded to gs://{bucket_name}/{destination_blob_name}') # noqa + except Exception as error: + logger.error(f"Output data not pushed to GCS. ERROR-INFO - {error}") + + +def download_json_from_gcs(project_id, bucket_name, source_blob_name): + try: + storage_client = storage.Client(project=project_id) + bucket = storage_client.bucket(bucket_name) + blob = bucket.blob(source_blob_name) + json_string = blob.download_as_string() + json_data = json.loads(json_string) + return json_data + except Exception as error: + logger.error(f"Target Servers output data couldn't be fetched. ERROR-INFO - {error}") # noqa + return None From 6746a8ced7112f3586f62323ac8c7cc262ec7bb9 Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Mon, 4 Mar 2024 10:29:11 +0530 Subject: [PATCH 04/12] fix: updated README and logger prints --- tools/target-server-validator/README.md | 9 +++++++++ tools/target-server-validator/apigee_utils.py | 2 +- tools/target-server-validator/main.py | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tools/target-server-validator/README.md b/tools/target-server-validator/README.md index 3fcfa948..f8e81df2 100644 --- a/tools/target-server-validator/README.md +++ b/tools/target-server-validator/README.md @@ -66,6 +66,15 @@ bucket_project_id=xx-xxx-xx # GCS bucket project id file_path_in_bucket=scan_output.txt # path to output file ``` +To get the notification channel id, use the following command + +``` +gcloud beta monitoring channels list --project= +``` + +This command will display all available notification channels within your project. You can select the appropriate one based on your requirements. Locate the notification channel ID under the `name` field in the format `projects//notificationChannels/`, and insert it into the input.properties file. + + * Sample input CSV with target servers > **NOTE:** You need to set `check_csv=true` in the `validation` section of `input.properties` diff --git a/tools/target-server-validator/apigee_utils.py b/tools/target-server-validator/apigee_utils.py index 133cf5c8..79f0b64f 100644 --- a/tools/target-server-validator/apigee_utils.py +++ b/tools/target-server-validator/apigee_utils.py @@ -212,7 +212,7 @@ def deploy_api_bundle(self, env, api_name, proxy_bundle_path, api_force_redeploy if self.get_api_revisions_deployment( env, api_name, api_rev ): - logger.debug(f"Proxy {api_name} active in runtime after {api_deployment_retry_count*api_deployment_sleep} seconds ") # noqa + logger.info(f"Proxy {api_name} active in runtime after {api_deployment_retry_count*api_deployment_sleep} seconds ") # noqa return True else: logger.debug(f"Checking API deployment status in {api_deployment_sleep} seconds") # noqa diff --git a/tools/target-server-validator/main.py b/tools/target-server-validator/main.py index 65ef61ea..23bb3cb9 100644 --- a/tools/target-server-validator/main.py +++ b/tools/target-server-validator/main.py @@ -172,7 +172,7 @@ def main(): for each_api_type, each_api_type_data in api_revision_map.items() # noqa for each_api, each_api_rev in each_api_type_data["proxies"].items() # noqa ) - logger.debug("Exporting proxy target servers") + logger.info("Exporting proxy target servers") results = run_parallel(source_apigee.fetch_api_proxy_ts_parallel, parallel_args) # noqa for result in results: @@ -192,7 +192,7 @@ def main(): proxy_targets[each_te] = [ f"{each_api_type} - {each_api}" ] - logger.debug("Exporting proxy target servers done") + logger.info("Exporting proxy target servers done") # Fetch API Northbound Endpoint logger.info(f"Fetching VHost with name {cfg['validation']['api_hostname']} !") # noqa From 9218f64247864495948f80252cebfda436ce4872 Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Tue, 5 Mar 2024 12:14:46 +0530 Subject: [PATCH 05/12] feat: added support for ADC and option to store scan output data in gcs or local file --- tools/target-server-validator/README.md | 17 ++++-- tools/target-server-validator/apigee_utils.py | 20 ++++++- .../target-server-validator/input.properties | 8 +-- tools/target-server-validator/main.py | 59 ++++++++++++------- tools/target-server-validator/utilities.py | 14 ++++- 5 files changed, 85 insertions(+), 33 deletions(-) diff --git a/tools/target-server-validator/README.md b/tools/target-server-validator/README.md index f8e81df2..d72870bc 100644 --- a/tools/target-server-validator/README.md +++ b/tools/target-server-validator/README.md @@ -60,10 +60,10 @@ dashboard_title=Apigee Target Server Monitoring Dashboard # Monitoring Dashboar alert_policy_name=Apigee Target Server Validator Policy # Alerting Policy Name notification_channel_id=xxxxxxxx # Notification Channel id -[gcs_bucket] -bucket_name=target-server-validator-gcs # GCS bucket name for storing the --scan output -bucket_project_id=xx-xxx-xx # GCS bucket project id -file_path_in_bucket=scan_output.txt # path to output file +[target_server_state_file] +state_file=gs://bucket_name/path/to/file/scan_output.json # GCS Bucket path to store --scan output +# state_file=file://scan_output.json # File path to store --scan output (only one can be used either GCS or file) +gcs_project_id=xxx-xxxx-xxx-xxxxx # GCS bucket project id ``` To get the notification channel id, use the following command @@ -88,7 +88,14 @@ smtp.gmail.com,465 ``` -* Please run below commands to authenticate, based on the Apigee flavours you are using. +* Please run below commands to authenticate, + +``` +gcloud auth application-default set-quota-project +``` +You can skip the quota-project if you want. + +Another way to authenticate is to use the environmnet variables based on the Apigee flavours. ``` export APIGEE_OPDK_ACCESS_TOKEN=$(echo -n ":" | base64) # Access token for Apigee OPDK diff --git a/tools/target-server-validator/apigee_utils.py b/tools/target-server-validator/apigee_utils.py index 79f0b64f..f437dd27 100644 --- a/tools/target-server-validator/apigee_utils.py +++ b/tools/target-server-validator/apigee_utils.py @@ -20,6 +20,8 @@ import requests import shutil from time import sleep +import google.auth +import google.auth.transport.requests from utilities import ( # pylint: disable=import-error run_validator_proxy, unzip_file, @@ -57,6 +59,18 @@ def is_token_valid(self, token): return False def get_access_token(self): + try: + credentials, project_id = google.auth.default() + request = google.auth.transport.requests.Request() + credentials.refresh(request) + access_token = credentials.token + if self.is_token_valid(access_token): + return access_token + logger.error('please run "export APIGEE_ACCESS_TOKEN=$(gcloud auth print-access-token)" first or set the Application Default Credentials using "gcloud auth application-default login" !! ') # noqa + except Exception as e: + logger.debug(f"Couldn't find the default credentials. ERROR-INFO :{e}") # noqa + + logger.debug("Checking env variable value.") token = os.getenv( "APIGEE_ACCESS_TOKEN" if self.apigee_type == "x" @@ -67,15 +81,15 @@ def get_access_token(self): if self.is_token_valid(token): return token else: - logger.error('please run "export APIGEE_ACCESS_TOKEN=$(gcloud auth print-access-token)" first !! ') # noqa + logger.error('please run "export APIGEE_ACCESS_TOKEN=$(gcloud auth print-access-token)" first or set the Application Default Credentials using "gcloud auth application-default login" !! ') # noqa sys.exit(1) else: return token else: if self.apigee_type == "x": - logger.error('please run "export APIGEE_ACCESS_TOKEN=$(gcloud auth print-access-token)" first !! ') # noqa + logger.error('please run "export APIGEE_ACCESS_TOKEN=$(gcloud auth print-access-token)" first or set the Application Default Credentials using "gcloud auth application-default login" !! ') # noqa else: - logger.error('please export APIGEE_OPDK_ACCESS_TOKEN') + logger.error('please export APIGEE_OPDK_ACCESS_TOKEN or set the Application Default Credentials using "gcloud auth application-default login"') # noqa sys.exit(1) def set_auth_header(self): diff --git a/tools/target-server-validator/input.properties b/tools/target-server-validator/input.properties index 5a2907fc..be592b70 100644 --- a/tools/target-server-validator/input.properties +++ b/tools/target-server-validator/input.properties @@ -34,7 +34,7 @@ dashboard_title=Apigee Target Server Health Monitoring Dashboard alert_policy_name=Apigee Target Server Validator Policy notification_channel_id=xxxxx -[gcs_bucket] -bucket_name=target-server-validator-gcs -bucket_project_id=xxx-xxx-xxx -file_path_in_bucket=scan_output.txt \ No newline at end of file +[target_server_state_file] +state_file=gs://bucket_name/path/to/file/scan_output.json +# state_file=file://scan_output.json +gcs_project_id=apigee-hybrid-testing-415007 \ No newline at end of file diff --git a/tools/target-server-validator/main.py b/tools/target-server-validator/main.py index 23bb3cb9..cded6916 100644 --- a/tools/target-server-validator/main.py +++ b/tools/target-server-validator/main.py @@ -36,6 +36,8 @@ get_metric_descriptor, gcs_upload_json, download_json_from_gcs, + write_json_to_file, + read_json_from_file, ) from apigee_utils import Apigee # pylint: disable=import-error from base_logger import logger @@ -59,15 +61,12 @@ def main(): enable_gcp_metrics = cfg["gcp_metrics"].getboolean("enable_gcp_metrics") report_format = cfg["validation"]["report_format"] allow_insecure = cfg["validation"].getboolean("allow_insecure") - project_id = cfg["gcp_metrics"]["project_id"] + metrics_project_id = cfg["gcp_metrics"]["project_id"] metric_name = cfg["gcp_metrics"]["metric_name"] enable_dashboard = cfg["gcp_metrics"]["enable_dashboard"] - vhost_domain_name = cfg["validation"]["api_hostname"] - vhost_ip = cfg["validation"].get("api_ip", "").strip() - api_url = f"https://{vhost_domain_name}/validate-target-server" - bucket_name = cfg["gcs_bucket"]["bucket_name"] - file_path_in_bucket = cfg["gcs_bucket"]["file_path_in_bucket"] - bucket_project_id = cfg["gcs_bucket"]["bucket_project_id"] + state_file = cfg["target_server_state_file"]["state_file"] + gcs_project_id = cfg["target_server_state_file"]["gcs_project_id"] + if report_format not in ["csv", "md"]: report_format = "md" @@ -109,14 +108,14 @@ def main(): # create metric descriptor and dashboard if enable_gcp_metrics: logger.info("Creating metric descriptor") - descriptor = create_custom_metric(project_id, metric_name) - # report_metric(project_id, descriptor, final_report) + descriptor = create_custom_metric(metrics_project_id, metric_name) + if enable_dashboard: - logger.info(f"Creating dashboard in project {project_id}") + logger.info(f"Creating dashboard in project {metrics_project_id}") # noqa dashboard_title = cfg["gcp_metrics"]["dashboard_title"] alert_policy_name = cfg["gcp_metrics"]["alert_policy_name"] notification_channel_id = cfg["gcp_metrics"]["notification_channel_id"] # noqa - create_custom_dashboard(project_id, dashboard_title, metric_name, alert_policy_name, notification_channel_id) # noqa + create_custom_dashboard(metrics_project_id, dashboard_title, metric_name, alert_policy_name, notification_channel_id) # noqa if args.scan: environments = source_apigee.list_environments() @@ -194,9 +193,6 @@ def main(): ] logger.info("Exporting proxy target servers done") - # Fetch API Northbound Endpoint - logger.info(f"Fetching VHost with name {cfg['validation']['api_hostname']} !") # noqa - for each_api_type, apis in proxy_hosts.items(): for each_api, each_targets in apis.items(): for each_target in each_targets: @@ -233,12 +229,29 @@ def main(): "proxy_targets": proxy_targets } - # upload output to gcs - gcs_upload_json(bucket_project_id, bucket_name, file_path_in_bucket, scan_output) # noqa + # upload scan output + if state_file.startswith("file"): + file_path = state_file.replace("file://", "") + write_json_to_file(file_path, scan_output) + elif state_file.startswith("gs"): + bucket_data = state_file.split('/') + bucket_name = bucket_data[2] + file_path = '/'.join(bucket_data[3:]) + + gcs_upload_json(gcs_project_id, bucket_name, file_path, scan_output) # noqa if args.monitor: - # Download scan ouput from gcs - scan_output = download_json_from_gcs(bucket_project_id, bucket_name, file_path_in_bucket) # noqa + + # extract scan outout + if state_file.startswith("file"): + file_path = state_file.replace("file://", "") + scan_output = read_json_from_file(file_path) + elif state_file.startswith("gs"): + bucket_data = state_file.split('/') + bucket_name = bucket_data[2] + file_path = '/'.join(bucket_data[3:]) + + scan_output = download_json_from_gcs(gcs_project_id, bucket_name, file_path) # noqa if not scan_output: return @@ -246,6 +259,12 @@ def main(): all_target_servers = scan_output["all_target_servers"] proxy_targets = scan_output["proxy_targets"] + # Fetch API Northbound Endpoint + logger.info(f"Fetching VHost with name {cfg['validation']['api_hostname']} !") # noqa + vhost_domain_name = cfg["validation"]["api_hostname"] + vhost_ip = cfg["validation"].get("api_ip", "").strip() + api_url = f"https://{vhost_domain_name}/validate-target-server" + batch_size = 5 batches = [] new_structure = [] @@ -296,9 +315,9 @@ def main(): if enable_gcp_metrics: logger.info("Dumping data to stack driver") # get metric descriptor - descriptor = get_metric_descriptor(project_id, metric_name) + descriptor = get_metric_descriptor(metrics_project_id, metric_name) if descriptor: - report_metric(project_id, descriptor, final_report) + report_metric(metrics_project_id, descriptor, final_report) else: logger.error("Couldn't push data to stackdriver because the the existing metric descriptor couldn't be fetched.") # noqa diff --git a/tools/target-server-validator/utilities.py b/tools/target-server-validator/utilities.py index b9ec2fcd..b280604e 100644 --- a/tools/target-server-validator/utilities.py +++ b/tools/target-server-validator/utilities.py @@ -23,6 +23,7 @@ import csv from urllib.parse import urlparse import time +import concurrent.futures from google.api import label_pb2 as ga_label from google.cloud import monitoring_v3 from google.api import metric_pb2 as ga_metric @@ -33,7 +34,6 @@ import xmltodict import urllib3 from forcediphttpsadapter.adapters import ForcedIPHTTPSAdapter -import concurrent.futures from base_logger import logger @@ -445,3 +445,15 @@ def download_json_from_gcs(project_id, bucket_name, source_blob_name): except Exception as error: logger.error(f"Target Servers output data couldn't be fetched. ERROR-INFO - {error}") # noqa return None + + +def write_json_to_file(file_path, data): + with open(file_path, 'w') as f: + json.dump(data, f) + + +def read_json_from_file(file_path): + with open(file_path, 'r') as f: + scan_output = json.load(f) + + return scan_output From 7f4e8e6fd2724e2cf2e1ccf41efd2d34247c2333 Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Tue, 5 Mar 2024 15:34:36 +0530 Subject: [PATCH 06/12] fix: updated requirements.txt and utilities.py --- tools/target-server-validator/README.md | 7 ++++--- tools/target-server-validator/requirements.txt | 1 + tools/target-server-validator/utilities.py | 18 ++++++++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/tools/target-server-validator/README.md b/tools/target-server-validator/README.md index d72870bc..2786112f 100644 --- a/tools/target-server-validator/README.md +++ b/tools/target-server-validator/README.md @@ -113,14 +113,15 @@ export APIGEE_ACCESS_TOKEN=$(gcloud auth print-access-token) # Access The script supports the below arguments -* `--onboard` Toggle to onboard validator proxy, custom metric descriptors and dashboard -* `--scan` Toggle to read all resources -* `--monitor` Toggle to check the status of target servers and push to GCP Logging +* `--onboard` option to create validator proxy, custom metric descriptors and dashboard +* `--scan` option to fetch target servers from Environment target servers, api proxies & csv file +* `--monitor` option to check the status of target servers and generate report or push to GCP metrics To onboard, run ``` python3 main.py --onboard ``` +Make sure you have build the java callout jar before running onboard. To scan, run ``` diff --git a/tools/target-server-validator/requirements.txt b/tools/target-server-validator/requirements.txt index 55bfd5b2..b24afe0d 100644 --- a/tools/target-server-validator/requirements.txt +++ b/tools/target-server-validator/requirements.txt @@ -21,3 +21,4 @@ forcediphttpsadapter==1.0.2 google-cloud-monitoring==2.19.1 google-cloud-monitoring-dashboards==2.14.1 google-cloud-storage==2.14.0 +google-api-python-client==2.120.0 diff --git a/tools/target-server-validator/utilities.py b/tools/target-server-validator/utilities.py index b280604e..769e50ca 100644 --- a/tools/target-server-validator/utilities.py +++ b/tools/target-server-validator/utilities.py @@ -448,12 +448,18 @@ def download_json_from_gcs(project_id, bucket_name, source_blob_name): def write_json_to_file(file_path, data): - with open(file_path, 'w') as f: - json.dump(data, f) + try: + with open(file_path, 'w') as f: + json.dump(data, f) + logger.info(f"Successfully written data to {file_path}") + except Exception as e: + logger.error(f"Data not written to {file_path}. ERROR-INFO: {e}") def read_json_from_file(file_path): - with open(file_path, 'r') as f: - scan_output = json.load(f) - - return scan_output + try: + with open(file_path, 'r') as f: + scan_output = json.load(f) + return scan_output + except Exception as e: + logger.error(f"Data couldn't be fetched from {file_path}. ERROR-INFO: {e}") # noqa From 258928fbe1d43cf23c3cb96904428497a92a5b50 Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Wed, 13 Mar 2024 00:26:20 +0530 Subject: [PATCH 07/12] fix: fixed README.md and get_status function --- tools/target-server-validator/README.md | 11 +++++++---- tools/target-server-validator/utilities.py | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/target-server-validator/README.md b/tools/target-server-validator/README.md index 2786112f..9112aa8f 100644 --- a/tools/target-server-validator/README.md +++ b/tools/target-server-validator/README.md @@ -10,8 +10,7 @@ Validation is done by deploying a sample proxy which check if HOST & PORT is ope ## Pre-Requisites * Python3.x * Java -* Maven -3.9.6 +* Maven >= 3.9.6 * Please install the required Python dependencies ``` python3 -m pip install -r requirements.txt @@ -52,7 +51,7 @@ api_ip= # IP address corresponding to a report_format=csv # Report Format. Choose csv or md (defaults to md) [gcp_metrics] -enable_gcp_metrics=true # set 'true' to push target server's host and status to stack driver +enable_gcp_metrics=true # set 'true' to push target server's host and status to GCP metrics project_id=xxx-xxx-xxx # Project id of GCP project where the data will be pushed metric_name=custom.googleapis.com/ # Replace with custom metric name enable_dashboard=true # set 'true' to create the dashboard with alerting policy @@ -174,4 +173,8 @@ Please check a [Sample report](report.md) ## GCP Monitoring Dashboard The script can also create a GCP Monitoring Dashboard with an alerting widget like shown below: -![GCP Monitoring Dashboard](images/dashboard.png) \ No newline at end of file +![GCP Monitoring Dashboard](images/dashboard.png) + +This script creates a custom metric with labels as hostname and status. The possible statuses, namely REACHABLE NOT_REACHABLE, and UNKNOWN_HOST, are determined by calling the validator proxy. These statuses are then assigned values of 1, 0.5, and 0, respectively. + +Then, an alerting policy is created with a threshold of 0.75. Entries below this threshold trigger alerts sent to designated notification channels. Finally, this policy is added as a widget on the GCP dashboard. \ No newline at end of file diff --git a/tools/target-server-validator/utilities.py b/tools/target-server-validator/utilities.py index 769e50ca..db2840dc 100644 --- a/tools/target-server-validator/utilities.py +++ b/tools/target-server-validator/utilities.py @@ -310,7 +310,7 @@ def create_custom_metric(project_id, metric_name): return None -def get_status_int(status): +def get_status_value(status): if status == "REACHABLE": return 1 elif status == "NOT_REACHABLE": @@ -339,7 +339,7 @@ def report_metric(project_id, metric_descriptor, sample_data): try: for data in sample_data: - point = monitoring_v3.Point({'interval': interval, 'value': {'double_value': get_status_int(data[5])}}) # noqa + point = monitoring_v3.Point({'interval': interval, 'value': {'double_value': get_status_value(data[5])}}) # noqa series.metric.labels['hostname'] = data[2] series.metric.labels['status'] = data[5] series.points = [point] From 0d75949453a92728450829aca8f1605076ef6ccc Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Wed, 20 Mar 2024 10:29:44 +0530 Subject: [PATCH 08/12] fix: fixed pipeline.sh --- tools/target-server-validator/.gitignore | 2 + tools/target-server-validator/README.md | 40 ++++++++++++-- tools/target-server-validator/apigee_utils.py | 2 +- .../generated.properties | 40 ++++++++++++++ .../target-server-validator/input.properties | 18 +++---- tools/target-server-validator/main.py | 10 ++-- tools/target-server-validator/pipeline.sh | 53 +++++++----------- .../test/create_notification_channel.sh | 26 +++++++++ .../test/email-channel.json | 8 +++ tools/target-server-validator/utilities.py | 54 ++++++++++--------- 10 files changed, 176 insertions(+), 77 deletions(-) create mode 100644 tools/target-server-validator/generated.properties create mode 100755 tools/target-server-validator/test/create_notification_channel.sh create mode 100644 tools/target-server-validator/test/email-channel.json diff --git a/tools/target-server-validator/.gitignore b/tools/target-server-validator/.gitignore index 3f7d48a5..919a2086 100644 --- a/tools/target-server-validator/.gitignore +++ b/tools/target-server-validator/.gitignore @@ -1,2 +1,4 @@ callout/target/ export/ +scan_output.json +input.csv \ No newline at end of file diff --git a/tools/target-server-validator/README.md b/tools/target-server-validator/README.md index 9112aa8f..0c1f8cc9 100644 --- a/tools/target-server-validator/README.md +++ b/tools/target-server-validator/README.md @@ -57,7 +57,7 @@ metric_name=custom.googleapis.com/ # Replace with cu enable_dashboard=true # set 'true' to create the dashboard with alerting policy dashboard_title=Apigee Target Server Monitoring Dashboard # Monitoring Dashboard Title alert_policy_name=Apigee Target Server Validator Policy # Alerting Policy Name -notification_channel_id=xxxxxxxx # Notification Channel id +notification_channel_ids=xxxxxxxx # Comma separated list of Notification Channel ids [target_server_state_file] state_file=gs://bucket_name/path/to/file/scan_output.json # GCS Bucket path to store --scan output @@ -115,23 +115,26 @@ The script supports the below arguments * `--onboard` option to create validator proxy, custom metric descriptors and dashboard * `--scan` option to fetch target servers from Environment target servers, api proxies & csv file * `--monitor` option to check the status of target servers and generate report or push to GCP metrics +* `--input` Path to input properties file To onboard, run ``` -python3 main.py --onboard +python3 main.py --input path/to/input_file --onboard ``` Make sure you have build the java callout jar before running onboard. To scan, run ``` -python3 main.py --scan +python3 main.py --input path/to/input_file --scan ``` To monitor, run ``` -python3 main.py --monitor +python3 main.py --input path/to/input_file --monitor ``` +You can also pass multiple arguments at the same time. + --onboard deploys an API proxy to validate if the target servers are reachable or not. To use the API proxy, make sure your payloads adhere to the following format: ```json @@ -177,4 +180,31 @@ The script can also create a GCP Monitoring Dashboard with an alerting widget li This script creates a custom metric with labels as hostname and status. The possible statuses, namely REACHABLE NOT_REACHABLE, and UNKNOWN_HOST, are determined by calling the validator proxy. These statuses are then assigned values of 1, 0.5, and 0, respectively. -Then, an alerting policy is created with a threshold of 0.75. Entries below this threshold trigger alerts sent to designated notification channels. Finally, this policy is added as a widget on the GCP dashboard. \ No newline at end of file +Then, an alerting policy is created with a threshold of 0.75. Entries below this threshold trigger alerts sent to designated notification channels. Finally, this policy is added as a widget on the GCP dashboard. + +# Running the Pipeline + +To run the pipeline script (`pipeline.sh`), follow these steps: + +## Prerequisites + +Before running the pipeline script, ensure you have the following prerequisites configured: + +- **Environment Variables**: Set up the necessary environment variables required by the script. These variables should include: + - `APIGEE_X_ORG`: Your Apigee organization ID. + - `APIGEE_X_ENV`: The Apigee environment to deploy to. + - `APIGEE_X_HOSTNAME`: The hostname for your Apigee instance. + + *NOTE*: This pipeline will create a test notification channel with type email and email_address as `no-reply@google.com`. + +- **Input Properties Template**: This script requires an `input.properties` file for the necessary configuration parameters and will create a corresponding `generated.properties` file by replacing the environment variables with their values. Ensure that the values are set properly in this file before running the script. + +## Running the Pipeline + +### Command + +To execute the pipeline, use the following command: + +``` +./pipeline.sh +``` \ No newline at end of file diff --git a/tools/target-server-validator/apigee_utils.py b/tools/target-server-validator/apigee_utils.py index f437dd27..8904d33e 100644 --- a/tools/target-server-validator/apigee_utils.py +++ b/tools/target-server-validator/apigee_utils.py @@ -221,7 +221,7 @@ def deploy_api_bundle(self, env, api_name, proxy_bundle_path, api_force_redeploy return True else: if self.deploy_api(env, api_name, api_rev): - logger.info(f"Proxy with name {api_name} has been deployed to {env} in Apigee Org {self.org}") # noqa + logger.info(f"Deploying proxy with name {api_name} to {env} in Apigee Org {self.org}") # noqa while api_deployment_retry_count < api_deployment_retry: if self.get_api_revisions_deployment( env, api_name, api_rev diff --git a/tools/target-server-validator/generated.properties b/tools/target-server-validator/generated.properties new file mode 100644 index 00000000..35f243a8 --- /dev/null +++ b/tools/target-server-validator/generated.properties @@ -0,0 +1,40 @@ +[source] +baseurl=https://apigee.googleapis.com/v1 +org=xxx-xxx-xxx +auth_type=oauth + +[target] +baseurl=https://apigee.googleapis.com/v1 +org=xxx-xxx-xxx +auth_type=oauth + +[csv] +file=input.csv +default_port=443 + +[validation] +check_csv=true +check_proxies=true +proxy_export_dir=export +skip_proxy_list=mock1,stream +api_env=dev +api_name=target-server-validator +api_force_redeploy=true +api_hostname=example.apigee.com +api_ip= +report_format=md +allow_insecure=false + +[gcp_metrics] +enable_gcp_metrics=true +project_id=xx-xxx-xxx +metric_name=custom.googleapis.com/host_status +enable_dashboard=true +dashboard_title=Apigee Target Server Health Monitoring Dashboard +alert_policy_name=Apigee Target Server Validator Policy +notification_channel_ids=xxxxx + +[target_server_state_file] +state_file=gs://bucket_name/path/to/file/scan_output.json +# state_file=file://scan_output.json +gcs_project_id=xx-xxx-xxx \ No newline at end of file diff --git a/tools/target-server-validator/input.properties b/tools/target-server-validator/input.properties index be592b70..732441ae 100644 --- a/tools/target-server-validator/input.properties +++ b/tools/target-server-validator/input.properties @@ -1,11 +1,11 @@ [source] baseurl=https://apigee.googleapis.com/v1 -org=xxx-xxx-xxx +org=${APIGEE_X_ORG} auth_type=oauth [target] baseurl=https://apigee.googleapis.com/v1 -org=xxx-xxx-xxx +org=${APIGEE_X_ORG} auth_type=oauth [csv] @@ -17,24 +17,24 @@ check_csv=true check_proxies=true proxy_export_dir=export skip_proxy_list=mock1,stream -api_env=dev +api_env=${APIGEE_X_ENV} api_name=target-server-validator api_force_redeploy=true -api_hostname=example.apigee.com +api_hostname=${APIGEE_X_HOSTNAME} api_ip= report_format=md allow_insecure=false [gcp_metrics] enable_gcp_metrics=true -project_id=xx-xxx-xxx +project_id=${APIGEE_X_ORG} metric_name=custom.googleapis.com/host_status enable_dashboard=true dashboard_title=Apigee Target Server Health Monitoring Dashboard alert_policy_name=Apigee Target Server Validator Policy -notification_channel_id=xxxxx +notification_channel_ids=${NOTIFICATION_CHANNEL_ID} [target_server_state_file] -state_file=gs://bucket_name/path/to/file/scan_output.json -# state_file=file://scan_output.json -gcs_project_id=apigee-hybrid-testing-415007 \ No newline at end of file +# state_file=gs://bucket_name/path/to/file/scan_output.json +state_file=file://scan_output.json +gcs_project_id=${APIGEE_X_ORG} \ No newline at end of file diff --git a/tools/target-server-validator/main.py b/tools/target-server-validator/main.py index cded6916..a55a16f3 100644 --- a/tools/target-server-validator/main.py +++ b/tools/target-server-validator/main.py @@ -51,11 +51,11 @@ def main(): parser.add_argument('--onboard', action='store_true', help='Toggle to onboard validator proxy, custom metric descriptors and dashboard') # noqa parser.add_argument('--scan', action='store_true', help='Toggle to read all resources') # noqa parser.add_argument('--monitor', action='store_true', help='Toggle to check the status of target servers and push to GCP Logging') # noqa - + parser.add_argument('--input', default='input.properties', help='Path to input file', type=str) # noqa args = parser.parse_args() # Parse Inputs - cfg = parse_config("input.properties") + cfg = parse_config(args.input) check_proxies = cfg["validation"].getboolean("check_proxies") proxy_export_dir = cfg["validation"]["proxy_export_dir"] enable_gcp_metrics = cfg["gcp_metrics"].getboolean("enable_gcp_metrics") @@ -114,8 +114,8 @@ def main(): logger.info(f"Creating dashboard in project {metrics_project_id}") # noqa dashboard_title = cfg["gcp_metrics"]["dashboard_title"] alert_policy_name = cfg["gcp_metrics"]["alert_policy_name"] - notification_channel_id = cfg["gcp_metrics"]["notification_channel_id"] # noqa - create_custom_dashboard(metrics_project_id, dashboard_title, metric_name, alert_policy_name, notification_channel_id) # noqa + notification_channel_ids = cfg["gcp_metrics"]["notification_channel_ids"] # noqa + create_custom_dashboard(metrics_project_id, dashboard_title, metric_name, alert_policy_name, notification_channel_ids) # noqa if args.scan: environments = source_apigee.list_environments() @@ -319,7 +319,7 @@ def main(): if descriptor: report_metric(metrics_project_id, descriptor, final_report) else: - logger.error("Couldn't push data to stackdriver because the the existing metric descriptor couldn't be fetched.") # noqa + logger.error("Couldn't push data to gcp metrics because the the existing metric descriptor couldn't be fetched.") # noqa elif report_format == "md": report_file = "report.md" diff --git a/tools/target-server-validator/pipeline.sh b/tools/target-server-validator/pipeline.sh index 3f65ba59..40689ae2 100755 --- a/tools/target-server-validator/pipeline.sh +++ b/tools/target-server-validator/pipeline.sh @@ -16,47 +16,30 @@ set -e -SCRIPTPATH="$( cd "$(dirname "$0")" || exit >/dev/null 2>&1 ; pwd -P )" +SCRIPTPATH="$( + cd "$(dirname "$0")" || exit >/dev/null 2>&1 + pwd -P +)" + +NOTIFICATION_CHANNEL_IDS=$(bash "$SCRIPTPATH/test/create_notification_channel.sh" "$APIGEE_X_ORG" 2>&1) +if [ -z "$NOTIFICATION_CHANNEL_IDS" ]; then + echo "Error creating notification channel" + exit 1 +else + echo "Notification Channel Id - ${NOTIFICATION_CHANNEL_IDS}" +fi bash "$SCRIPTPATH/callout/build_java_callout.sh" # Clean up previously generated files -rm -rf "$SCRIPTPATH/input.properties" rm -rf "$SCRIPTPATH/export" rm -rf "$SCRIPTPATH/report*" # Generate input file -cat > "$SCRIPTPATH/input.properties" << EOF -[source] -baseurl=https://apigee.googleapis.com/v1 -org=$APIGEE_X_ORG -auth_type=oauth - -[target] -baseurl=https://apigee.googleapis.com/v1 -org=$APIGEE_X_ORG -auth_type=oauth - -[csv] -file=input.csv -default_port=443 - -[validation] -check_csv=true -check_proxies=true -proxy_export_dir=export -skip_proxy_list= -api_env=$APIGEE_X_ENV -api_name=target_server_validator -api_force_redeploy=true -api_hostname=$APIGEE_X_HOSTNAME -api_ip= -report_format=md -allow_insecure=false -EOF +envsubst <"$SCRIPTPATH/input.properties" >"$SCRIPTPATH/generated.properties" # Generate optional input csv file -cat > "$SCRIPTPATH/input.csv" << EOF +cat >"$SCRIPTPATH/input.csv" <&1) + +if [[ $list_response == "[]" ]]; then + + # create if not existing + create_response=$(gcloud beta monitoring channels create --channel-content-from-file="$JSON_FILE" --project="$PROJECT_ID" 2>&1) + + if [ $? -eq 0 ]; then + channel_id=$(echo "$create_response" | grep -oE "projects/"$PROJECT_ID"/notificationChannels/[0-9]+" | awk -F'/' '{print $4}') + echo "$channel_id" + else + echo + fi +else + comma_separated=$(echo "$list_response" | jq -r '.[].name' | awk -F'/' '{print $4}' | paste -sd "," -) + echo "$comma_separated" +fi diff --git a/tools/target-server-validator/test/email-channel.json b/tools/target-server-validator/test/email-channel.json new file mode 100644 index 00000000..85a1136b --- /dev/null +++ b/tools/target-server-validator/test/email-channel.json @@ -0,0 +1,8 @@ +{ + "type": "email", + "displayName": "Alert Notification Channel", + "description": "Email channel for alert notifications", + "labels": { + "email_address": "no-reply@google.com" + } +} \ No newline at end of file diff --git a/tools/target-server-validator/utilities.py b/tools/target-server-validator/utilities.py index db2840dc..5201aca5 100644 --- a/tools/target-server-validator/utilities.py +++ b/tools/target-server-validator/utilities.py @@ -326,7 +326,7 @@ def report_metric(project_id, metric_descriptor, sample_data): # Check if metric descriptor exists if not metric_descriptor: - logger.error("Error while pushing the data to stackdriver. ERROR-INFO: Metric descriptor does not exist.") # noqa + logger.error("Error while pushing the data to gcp metrics. ERROR-INFO: Metric descriptor does not exist.") # noqa return series.metric.type = metric_descriptor.type @@ -345,13 +345,13 @@ def report_metric(project_id, metric_descriptor, sample_data): series.points = [point] client.create_time_series(name=f"projects/{project_id}", time_series=[series]) # noqa - logger.debug(f"Pushed to stackdriver - {data[2]} {data[5]}") - logger.info("Successfully pushed data to stackdriver") + logger.debug(f"Pushed to gcp metrics - {data[2]} {data[5]}") + logger.info("Successfully pushed data to gcp metrics") except Exception as e: - logger.error(f"Error while pushing the data to stackdriver. ERROR-INFO: {e}") # noqa + logger.error(f"Error while pushing the data to gcp metrics. ERROR-INFO: {e}") # noqa -def create_alert_policy(project_id, policy_name, metric_name, notification_channel_id): # noqa +def create_alert_policy(project_id, policy_name, metric_name, notification_channel_ids): # noqa client = monitoring_v3.AlertPolicyServiceClient() conditions = [ monitoring_v3.AlertPolicy.Condition( @@ -376,7 +376,7 @@ def create_alert_policy(project_id, policy_name, metric_name, notification_chann ) ] - notification_channels = [f"projects/{project_id}/notificationChannels/{notification_channel_id}"] # noqa + notification_channels = [f"projects/{project_id}/notificationChannels/{notification_channel_id}" for notification_channel_id in notification_channel_ids.split(",")] # noqa policy = monitoring_v3.AlertPolicy( display_name=policy_name, conditions=conditions, @@ -384,15 +384,19 @@ def create_alert_policy(project_id, policy_name, metric_name, notification_chann combiner=monitoring_v3.AlertPolicy.ConditionCombinerType.OR, ) - created_policy = client.create_alert_policy( - name=f"projects/{project_id}", - alert_policy=policy - ) - logger.info(f"Created alert policy: {created_policy.name}") - return created_policy.name + try: + created_policy = client.create_alert_policy( + name=f"projects/{project_id}", + alert_policy=policy + ) + logger.info(f"Created alert policy: {created_policy.name}") + return created_policy.name + except Exception as e: + logger.error(f"Alerting Policy couldn't be created. ERROR-INFO - {e}") + return None -def create_custom_dashboard(project_id, dashboard_title, metric_name, policy_name, notification_channel_id): # noqa +def create_custom_dashboard(project_id, dashboard_title, metric_name, policy_name, notification_channel_ids): # noqa client = monitoring_dashboard_v1.DashboardsServiceClient() request = monitoring_dashboard_v1.ListDashboardsRequest(parent=f"projects/{project_id}") # noqa @@ -410,17 +414,19 @@ def create_custom_dashboard(project_id, dashboard_title, metric_name, policy_nam dashboard.grid_layout = grid_layout # create alerting policy - alert_policy_name = create_alert_policy(project_id, policy_name, metric_name, notification_channel_id) # noqa - widget = monitoring_dashboard_v1.Widget() - widget.alert_chart = monitoring_dashboard_v1.AlertChart(name=alert_policy_name) # noqa - dashboard.grid_layout.widgets.append(widget) - - request = monitoring_dashboard_v1.CreateDashboardRequest( - parent=f"projects/{project_id}", - dashboard=dashboard, - ) - response = client.create_dashboard(request=request) - logger.info(f"Dashboard created: {response.name}") + alert_policy_name = create_alert_policy(project_id, policy_name, metric_name, notification_channel_ids) # noqa + if alert_policy_name: + widget = monitoring_dashboard_v1.Widget() + widget.alert_chart = monitoring_dashboard_v1.AlertChart(name=alert_policy_name) # noqa + dashboard.grid_layout.widgets.append(widget) + request = monitoring_dashboard_v1.CreateDashboardRequest( + parent=f"projects/{project_id}", + dashboard=dashboard, + ) + response = client.create_dashboard(request=request) + logger.info(f"Dashboard created: {response.name}") + else: + logger.error("Dashboard could not be created, since alerting policy doesn't exist") # noqa def gcs_upload_json(project_id, bucket_name, destination_blob_name, json_data): From f8333ef98f28f044385ba0498c0e9f4e363d7e64 Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Wed, 20 Mar 2024 12:08:18 +0530 Subject: [PATCH 09/12] fix: fixed linting and license headers --- .../test/create_notification_channel.sh | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/tools/target-server-validator/test/create_notification_channel.sh b/tools/target-server-validator/test/create_notification_channel.sh index 78051387..fdb5ed1f 100755 --- a/tools/target-server-validator/test/create_notification_channel.sh +++ b/tools/target-server-validator/test/create_notification_channel.sh @@ -1,4 +1,18 @@ -#!/bin/bash +#!/bin/sh + +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. JSON_FILE="./test/email-channel.json" PROJECT_ID=$1 @@ -7,15 +21,16 @@ type=$(jq -r '.type' $JSON_FILE) displayName=$(jq -r '.displayName' $JSON_FILE) emailAddress=$(jq -r '.labels.email_address' $JSON_FILE) -list_response=$(gcloud alpha monitoring channels list --filter="type='$type' AND displayName=\"$displayName\" AND labels.email_address='$emailAddress'" --format=json --verbosity=none --project=$PROJECT_ID --format=json 2>&1) +list_response=$(gcloud alpha monitoring channels list --filter="type='$type' AND displayName=\"$displayName\" AND labels.email_address='$emailAddress'" --format=json --verbosity=none --project="$PROJECT_ID" --format=json 2>&1) -if [[ $list_response == "[]" ]]; then +if [ "$list_response" = "[]" ]; then # create if not existing create_response=$(gcloud beta monitoring channels create --channel-content-from-file="$JSON_FILE" --project="$PROJECT_ID" 2>&1) - - if [ $? -eq 0 ]; then - channel_id=$(echo "$create_response" | grep -oE "projects/"$PROJECT_ID"/notificationChannels/[0-9]+" | awk -F'/' '{print $4}') + exit_status=$? + + if [ "$exit_status" -eq 0 ]; then + channel_id=$(echo "$create_response" | grep -oE "projects/$PROJECT_ID/notificationChannels/[0-9]+" | awk -F'/' '{print $4}') echo "$channel_id" else echo From 4e1b9cccd19aff3e380fec783f14f81dad6d2e73 Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Fri, 22 Mar 2024 02:10:19 +0530 Subject: [PATCH 10/12] feat: added offboard option to delete proxy, gcp metrics and dashboards --- tools/target-server-validator/README.md | 13 +++- tools/target-server-validator/apigee_utils.py | 42 +++++++++++++ tools/target-server-validator/main.py | 12 +++- tools/target-server-validator/pipeline.sh | 5 ++ .../test/delete_notification_channel.sh | 24 +++++++ tools/target-server-validator/utilities.py | 63 +++++++++++++++++++ 6 files changed, 157 insertions(+), 2 deletions(-) create mode 100755 tools/target-server-validator/test/delete_notification_channel.sh diff --git a/tools/target-server-validator/README.md b/tools/target-server-validator/README.md index 0c1f8cc9..17f31cbb 100644 --- a/tools/target-server-validator/README.md +++ b/tools/target-server-validator/README.md @@ -11,6 +11,9 @@ Validation is done by deploying a sample proxy which check if HOST & PORT is ope * Python3.x * Java * Maven >= 3.9.6 + +* If you are pushing the data to gcp metrics, you require `roles/monitoring.editor` role. + * Please install the required Python dependencies ``` python3 -m pip install -r requirements.txt @@ -112,9 +115,10 @@ export APIGEE_ACCESS_TOKEN=$(gcloud auth print-access-token) # Access The script supports the below arguments -* `--onboard` option to create validator proxy, custom metric descriptors and dashboard +* `--onboard` option to create validator proxy, custom metric descriptors, alerting policy and dashboard * `--scan` option to fetch target servers from Environment target servers, api proxies & csv file * `--monitor` option to check the status of target servers and generate report or push to GCP metrics +* `--offboard` option to delete validator proxy, custom metric descriptors, alerting policy and dashboard * `--input` Path to input properties file To onboard, run @@ -133,6 +137,11 @@ To monitor, run python3 main.py --input path/to/input_file --monitor ``` +To offboard, run +``` +python3 main.py --input path/to/input_file --offboard +``` + You can also pass multiple arguments at the same time. --onboard deploys an API proxy to validate if the target servers are reachable or not. To use the API proxy, make sure your payloads adhere to the following format: @@ -197,6 +206,8 @@ Before running the pipeline script, ensure you have the following prerequisites *NOTE*: This pipeline will create a test notification channel with type email and email_address as `no-reply@google.com`. +- **IAM Roles**: To set up the monitoring dashboard and alerts, make sure that you have `roles/monitoring.editor` role. + - **Input Properties Template**: This script requires an `input.properties` file for the necessary configuration parameters and will create a corresponding `generated.properties` file by replacing the environment variables with their values. Ensure that the values are set properly in this file before running the script. ## Running the Pipeline diff --git a/tools/target-server-validator/apigee_utils.py b/tools/target-server-validator/apigee_utils.py index 8904d33e..42534d07 100644 --- a/tools/target-server-validator/apigee_utils.py +++ b/tools/target-server-validator/apigee_utils.py @@ -156,6 +156,48 @@ def create_api(self, api_name, proxy_bundle_path): logger.debug(response.text) return False, None + def get_api_deployments(self, api_name): + headers = self.auth_header.copy() + + deployed_revision_url = f"{self.baseurl}/apis/{api_name}/deployments" + deployed_revision_get_response = requests.request( + "GET", deployed_revision_url, headers=headers, data={} + ) + deployments = deployed_revision_get_response.json() + revision_deployements = deployments.get('deployments') + return revision_deployements + + def delete_api(self, api_name): + headers = self.auth_header.copy() + revision_deployements = self.get_api_deployments(api_name) + + if revision_deployements: + for revision_deployement in revision_deployements: + deployed_env = revision_deployement.get('environment') + rev = revision_deployement.get('revision') + + # delete api deployment + revision_delete_url = f"{self.baseurl}/environments/{deployed_env}/apis/{api_name}/revisions/{rev}/deployments" # noqa + revision_response = requests.request( + "DELETE", + revision_delete_url, headers=headers, data={} + ) + if revision_response.status_code == 200: + logger.info(f"Successfully deleted {api_name} api proxy revision {rev} in env {deployed_env}") # noqa + + # proxy deletion + url = f"{self.baseurl}/apis/{api_name}" + try: + response = requests.request( + "DELETE", url, headers=headers, data={} + ) + if response.status_code == 200: + logger.info(f"Api proxy {api_name} deleted successfully.") + else: + logger.error(f"Error deleting Api proxy {api_name}. ERROR-INFO - {response.json()}") # noqa + except Exception as e: + logger.error(f"Couldn't delete api proxy {api_name}. ERROR-INFO- {e}") # noqa + def get_api_revisions_deployment(self, env, api_name, api_rev): # noqa url = ( url diff --git a/tools/target-server-validator/main.py b/tools/target-server-validator/main.py index a55a16f3..83650706 100644 --- a/tools/target-server-validator/main.py +++ b/tools/target-server-validator/main.py @@ -34,6 +34,9 @@ report_metric, create_custom_dashboard, get_metric_descriptor, + delete_alerting_policy, + delete_dashboard, + delete_metric_descriptor, gcs_upload_json, download_json_from_gcs, write_json_to_file, @@ -48,9 +51,10 @@ def main(): # Arguments parser = argparse.ArgumentParser(description='details', usage='use "%(prog)s --help" for more information',formatter_class=argparse.RawTextHelpFormatter) # noqa - parser.add_argument('--onboard', action='store_true', help='Toggle to onboard validator proxy, custom metric descriptors and dashboard') # noqa + parser.add_argument('--onboard', action='store_true', help='Toggle to onboard validator proxy, custom metric descriptors, dashboard and alerting policy') # noqa parser.add_argument('--scan', action='store_true', help='Toggle to read all resources') # noqa parser.add_argument('--monitor', action='store_true', help='Toggle to check the status of target servers and push to GCP Logging') # noqa + parser.add_argument('--offboard', action='store_true', help='Toggle to offboard validator proxy, custom metric descriptors, dashboard and alerting policy') # noqa parser.add_argument('--input', default='input.properties', help='Path to input file', type=str) # noqa args = parser.parse_args() @@ -333,6 +337,12 @@ def main(): logger.info(f"Dumping report to file {report_file}") write_csv_report(report_file, final_report) + if args.offboard: + target_apigee.delete_api(cfg["validation"]["api_name"]) + alerting_policies = delete_alerting_policy(cfg["target"]["org"], metric_name) # noqa + delete_dashboard(cfg["target"]["org"], alerting_policies) + delete_metric_descriptor(metric_name, cfg["target"]["org"]) + if __name__ == "__main__": main() diff --git a/tools/target-server-validator/pipeline.sh b/tools/target-server-validator/pipeline.sh index 40689ae2..55ee8bb4 100755 --- a/tools/target-server-validator/pipeline.sh +++ b/tools/target-server-validator/pipeline.sh @@ -67,9 +67,14 @@ python3 main.py --scan --input "$SCRIPTPATH/generated.properties" python3 main.py --monitor --input "$SCRIPTPATH/generated.properties" +python3 main.py --offboard --input "$SCRIPTPATH/generated.properties" + # Display Report cat "$SCRIPTPATH/report.md" +# delete notification channel +bash "$SCRIPTPATH/test/delete_notification_channel.sh" "$APIGEE_X_ORG" "$NOTIFICATION_CHANNEL_IDS" + # deactivate venv & cleanup deactivate rm -rf "$VENV_PATH" diff --git a/tools/target-server-validator/test/delete_notification_channel.sh b/tools/target-server-validator/test/delete_notification_channel.sh new file mode 100755 index 00000000..e911bbd1 --- /dev/null +++ b/tools/target-server-validator/test/delete_notification_channel.sh @@ -0,0 +1,24 @@ +#!/bin/sh + +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +PROJECT_ID=$1 +NOTIFICATION_CHANNEL_IDS=$2 +IFS=',' + +# Loop through each notification channel ID and delete it +for CHANNEL_ID in $NOTIFICATION_CHANNEL_IDS; do + gcloud beta monitoring channels delete "$CHANNEL_ID" --project="$PROJECT_ID" --quiet +done diff --git a/tools/target-server-validator/utilities.py b/tools/target-server-validator/utilities.py index 5201aca5..1615e82b 100644 --- a/tools/target-server-validator/utilities.py +++ b/tools/target-server-validator/utilities.py @@ -429,6 +429,69 @@ def create_custom_dashboard(project_id, dashboard_title, metric_name, policy_nam logger.error("Dashboard could not be created, since alerting policy doesn't exist") # noqa +def delete_dashboard(project_id, alerting_policies): + logger.info("Deleting GCP Monitoring Dashboard") + client = monitoring_dashboard_v1.DashboardsServiceClient() + + list_request = monitoring_dashboard_v1.ListDashboardsRequest( + parent=f"projects/{project_id}",) + list_response = client.list_dashboards(request=list_request) + + try: + for dashboard in list_response.dashboards: + if 'grid_layout' in dashboard and 'widgets' in dashboard.grid_layout: # noqa + for widget in dashboard.grid_layout.widgets: + if 'alert_chart' in widget: + alerting_policy = widget.alert_chart.name + if alerting_policy in alerting_policies: + delete_request = monitoring_dashboard_v1.DeleteDashboardRequest( # noqa + name=dashboard.name + ) + client.delete_dashboard(request=delete_request) + logger.info(f"Deleted monitoring dashboard {dashboard.name}") # noqa + except Exception as e: + logger.error(f"Error deleting dashboard: {e}") + + +def delete_alerting_policy(project_id, metric_name): + logger.info(f"Deleting alerting policy with metric {metric_name}") + try: + client = monitoring_v3.AlertPolicyServiceClient() + list_request = monitoring_v3.ListAlertPoliciesRequest( + name=f"projects/{project_id}", + ) + policies_list = client.list_alert_policies(request=list_request) + alerting_policy = [] + for alert_policy in policies_list.alert_policies: + if 'conditions' in alert_policy: + for condition in alert_policy.conditions: + if 'condition_threshold' in condition and 'filter' in condition.condition_threshold: # noqa + if f'metric.type = "{metric_name}"' in condition.condition_threshold.filter: # noqa + request = monitoring_v3.DeleteAlertPolicyRequest( + name=alert_policy.name, + ) + client.delete_alert_policy(request=request) + alerting_policy.append(alert_policy.name) + logger.info(f"Deleted alerting policy {alert_policy.name}") # noqa + return alerting_policy + except Exception as e: + logger.error(f"Couldn't delete alerting policy {alert_policy.name}. ERROR-INFO: {e}") # noqa + return [] + + +def delete_metric_descriptor(metric_name, project_id): + logger.info(f"Deleting metric descriptor {metric_name}") + client = monitoring_v3.MetricServiceClient() + request = monitoring_v3.DeleteMetricDescriptorRequest( + name=f"projects/{project_id}/metricDescriptors/{metric_name}", + ) + try: + client.delete_metric_descriptor(request=request) + logger.info(f"Deleted Metric Descriptor - {metric_name}") + except Exception as e: + logger.error(f"Couldn't delete metric descriptor {metric_name}. ERROR-INFO - {e}") # noqa + + def gcs_upload_json(project_id, bucket_name, destination_blob_name, json_data): try: storage_client = storage.Client(project=project_id) From a0ec366159f4981966b4063c05d9a4d5c52c10fb Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Fri, 22 Mar 2024 16:06:34 +0530 Subject: [PATCH 11/12] fix: added install gcloud beta component in pipeline.sh --- tools/target-server-validator/pipeline.sh | 3 ++- .../test/create_notification_channel.sh | 2 +- .../test/delete_notification_channel.sh | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/target-server-validator/pipeline.sh b/tools/target-server-validator/pipeline.sh index 55ee8bb4..7b8f46f2 100755 --- a/tools/target-server-validator/pipeline.sh +++ b/tools/target-server-validator/pipeline.sh @@ -21,12 +21,13 @@ SCRIPTPATH="$( pwd -P )" +gcloud components install beta --quiet # as the cloud-sdk image no longer has this NOTIFICATION_CHANNEL_IDS=$(bash "$SCRIPTPATH/test/create_notification_channel.sh" "$APIGEE_X_ORG" 2>&1) if [ -z "$NOTIFICATION_CHANNEL_IDS" ]; then echo "Error creating notification channel" exit 1 else - echo "Notification Channel Id - ${NOTIFICATION_CHANNEL_IDS}" + echo "Created Notification Channel Id - ${NOTIFICATION_CHANNEL_IDS}" fi bash "$SCRIPTPATH/callout/build_java_callout.sh" diff --git a/tools/target-server-validator/test/create_notification_channel.sh b/tools/target-server-validator/test/create_notification_channel.sh index fdb5ed1f..cbb91acb 100755 --- a/tools/target-server-validator/test/create_notification_channel.sh +++ b/tools/target-server-validator/test/create_notification_channel.sh @@ -21,7 +21,7 @@ type=$(jq -r '.type' $JSON_FILE) displayName=$(jq -r '.displayName' $JSON_FILE) emailAddress=$(jq -r '.labels.email_address' $JSON_FILE) -list_response=$(gcloud alpha monitoring channels list --filter="type='$type' AND displayName=\"$displayName\" AND labels.email_address='$emailAddress'" --format=json --verbosity=none --project="$PROJECT_ID" --format=json 2>&1) +list_response=$(gcloud beta monitoring channels list --filter="type='$type' AND displayName=\"$displayName\" AND labels.email_address='$emailAddress'" --format=json --verbosity=none --project="$PROJECT_ID" 2>&1) if [ "$list_response" = "[]" ]; then diff --git a/tools/target-server-validator/test/delete_notification_channel.sh b/tools/target-server-validator/test/delete_notification_channel.sh index e911bbd1..78199a3e 100755 --- a/tools/target-server-validator/test/delete_notification_channel.sh +++ b/tools/target-server-validator/test/delete_notification_channel.sh @@ -20,5 +20,6 @@ IFS=',' # Loop through each notification channel ID and delete it for CHANNEL_ID in $NOTIFICATION_CHANNEL_IDS; do - gcloud beta monitoring channels delete "$CHANNEL_ID" --project="$PROJECT_ID" --quiet + yes | gcloud beta monitoring channels delete "$CHANNEL_ID" --project="$PROJECT_ID" \ + || echo "Couldn't delete Notification Channel" done From 87e2d55e913e28588d7eb88bba6dfb3d7da3229a Mon Sep 17 00:00:00 2001 From: Payal Jindal Date: Fri, 22 Mar 2024 17:46:01 +0530 Subject: [PATCH 12/12] fix: fixed create notification channel script --- tools/target-server-validator/input.properties | 2 +- tools/target-server-validator/pipeline.sh | 16 +++++++--------- .../test/create_notification_channel.sh | 9 +++++---- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/tools/target-server-validator/input.properties b/tools/target-server-validator/input.properties index 732441ae..d255f6b7 100644 --- a/tools/target-server-validator/input.properties +++ b/tools/target-server-validator/input.properties @@ -32,7 +32,7 @@ metric_name=custom.googleapis.com/host_status enable_dashboard=true dashboard_title=Apigee Target Server Health Monitoring Dashboard alert_policy_name=Apigee Target Server Validator Policy -notification_channel_ids=${NOTIFICATION_CHANNEL_ID} +notification_channel_ids=${NOTIFICATION_CHANNEL_IDS} [target_server_state_file] # state_file=gs://bucket_name/path/to/file/scan_output.json diff --git a/tools/target-server-validator/pipeline.sh b/tools/target-server-validator/pipeline.sh index 7b8f46f2..ea1a62f3 100755 --- a/tools/target-server-validator/pipeline.sh +++ b/tools/target-server-validator/pipeline.sh @@ -22,13 +22,9 @@ SCRIPTPATH="$( )" gcloud components install beta --quiet # as the cloud-sdk image no longer has this -NOTIFICATION_CHANNEL_IDS=$(bash "$SCRIPTPATH/test/create_notification_channel.sh" "$APIGEE_X_ORG" 2>&1) -if [ -z "$NOTIFICATION_CHANNEL_IDS" ]; then - echo "Error creating notification channel" - exit 1 -else - echo "Created Notification Channel Id - ${NOTIFICATION_CHANNEL_IDS}" -fi +bash "$SCRIPTPATH/test/create_notification_channel.sh" "$APIGEE_X_ORG" "$SCRIPTPATH/channel.txt" +NOTIFICATION_CHANNEL_IDS=$(cat "$SCRIPTPATH/channel.txt") +echo "Created Notification Channel Id - $NOTIFICATION_CHANNEL_IDS" bash "$SCRIPTPATH/callout/build_java_callout.sh" @@ -37,7 +33,7 @@ rm -rf "$SCRIPTPATH/export" rm -rf "$SCRIPTPATH/report*" # Generate input file -envsubst <"$SCRIPTPATH/input.properties" >"$SCRIPTPATH/generated.properties" +NOTIFICATION_CHANNEL_IDS=$NOTIFICATION_CHANNEL_IDS envsubst <"$SCRIPTPATH/input.properties" >"$SCRIPTPATH/generated.properties" # Generate optional input csv file cat >"$SCRIPTPATH/input.csv" < "$OUTPUT_FILE" \ No newline at end of file