diff --git a/.gitignore b/.gitignore index 1b72049..5e89846 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ # IntelliJ IDE files .idea/ -*.iml \ No newline at end of file +*.iml +__pycache__ \ No newline at end of file diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-dns/custom.remote.python.thirdparty_dns.zip b/third-party-synthetic/active-gate-extensions/extension-third-party-dns/custom.remote.python.thirdparty_dns.zip index 37c782d..47cc74a 100644 Binary files a/third-party-synthetic/active-gate-extensions/extension-third-party-dns/custom.remote.python.thirdparty_dns.zip and b/third-party-synthetic/active-gate-extensions/extension-third-party-dns/custom.remote.python.thirdparty_dns.zip differ diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/dns_extension.py b/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/dns_extension.py index e414992..730627f 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/dns_extension.py +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/dns_extension.py @@ -7,13 +7,17 @@ from dynatrace import Dynatrace from dynatrace.environment_v1.synthetic_third_party import SYNTHETIC_EVENT_TYPE_OUTAGE +from dns_imports.environment import get_api_url + log = logging.getLogger(__name__) +DT_TIMEOUT_SECONDS = 10 class DNSExtension(RemoteBasePlugin): def initialize(self, **kwargs): # The Dynatrace API client - self.dt_client = Dynatrace(self.config.get("api_url"), self.config.get("api_token"), log=log, proxies=self.build_proxy_url()) + self.api_url = get_api_url() + self.dt_client = Dynatrace(self.api_url, self.config.get("api_token"), log=log, proxies=self.build_proxy_url(), timeout=DT_TIMEOUT_SECONDS) self.executions = 0 self.failures_detected = 0 @@ -48,7 +52,8 @@ def query(self, **kwargs) -> None: failure_count = self.config.get("failure_count", 1) if self.executions % frequency == 0: - success, response_time = test_dns(dns_server, host) + timeout = int(self.config.get("test_timeout", 2)) or 2 + success, response_time = test_dns(dns_server, host, timeout) log.info(f"DNS test, DNS server: {dns_server}, host: {host}, success: {success}, time: {response_time} ") if not success: @@ -59,39 +64,42 @@ def query(self, **kwargs) -> None: else: self.failures_detected = 0 - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( - engine_name="DNS", - timestamp=datetime.now(), - location_id=location_id, - location_name=location, - test_id=self.activation.entity_id, - test_title=test_title, - step_title=step_title, - schedule_interval=frequency * 60, - success=success, - response_time=response_time, - edit_link=f"#settings/customextension;id={self.plugin_info.name}", - icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-dns/dns.png", - ) - - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( - test_id=self.activation.entity_id, - name=f"DNS lookup failed for {step_title}", - location_id=location_id, - timestamp=datetime.now(), - state="open" if not success else "resolved", - event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, - reason=f"DNS lookup failed for {step_title}", - engine_name="DNS", - ) + try: + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( + engine_name="DNS", + timestamp=datetime.now(), + location_id=location_id, + location_name=location, + test_id=self.activation.entity_id, + test_title=test_title, + step_title=step_title, + schedule_interval=frequency * 60, + success=success, + response_time=response_time, + edit_link=f"#settings/customextension;id={self.plugin_info.name}", + icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-dns/dns.png", + ) + + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( + test_id=self.activation.entity_id, + name=f"DNS lookup failed for {step_title}", + location_id=location_id, + timestamp=datetime.now(), + state="open" if not success else "resolved", + event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, + reason=f"DNS lookup failed for {step_title}", + engine_name="DNS", + ) + except Exception as e: + self.logger.error(f"Error reporting third party test results to {self.api_url}: '{e}'") self.executions += 1 -def test_dns(dns_server: str, host: str) -> (bool, int): +def test_dns(dns_server: str, host: str, timeout: int) -> (bool, int): res = resolver.Resolver(configure=False) res.nameservers = [dns_server] - res.lifetime = res.timeout = 2 + res.lifetime = res.timeout = timeout start = datetime.now() try: diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/dns_imports/__init__.py b/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/dns_imports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/dns_imports/environment.py b/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/dns_imports/environment.py new file mode 100644 index 0000000..b4e9997 --- /dev/null +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/dns_imports/environment.py @@ -0,0 +1,42 @@ +import os +import re +from pathlib import Path +import logging + +log = logging.getLogger(__name__) + +tenant_regex = re.compile(r"\[(.*)\]") + + +def get_api_url() -> str: + endpoint = "" + environment = "" + + base_path = Path(__file__) + + # Only true if running from simulator + if "remotepluginmodule" not in str(base_path): + base_path = "/opt" if os.name != "nt" else "C:/Program Files" + base_path = Path(base_path) / "dynatrace" / "remotepluginmodule" / "agent" + + # Go up one level until the directory name is remotepluginmodule + while True: + base_path = base_path.parent + if base_path.name == "remotepluginmodule": + extensions_conf = base_path / "agent" / "conf" / "extensions.conf" + with open(extensions_conf, "r", errors="replace") as f: + for line in f: + if line.startswith("Server "): + endpoint = line.split(" ")[1].strip() + endpoint = endpoint.replace("/communication", "") + if line.startswith("Tenant "): + environment = line.split(" ")[1].strip() + if endpoint and environment: + api_url = f"{endpoint}/e/{environment}" + log.info(f"Found API URL: '{api_url}' in '{extensions_conf}'") + return api_url + else: + raise Exception(f"Could not find API URL after reading {extensions_conf}") + # End of the line + if base_path.parent == base_path: + raise Exception("Could not find config directory") diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/plugin.json b/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/plugin.json index 55544c9..a02d563 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/plugin.json +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-dns/src/plugin.json @@ -1,6 +1,6 @@ { "name": "custom.remote.python.thirdparty_dns", - "version": "1.015", + "version": "1.021", "type": "python", "entity": "CUSTOM_DEVICE", "technologies": [ @@ -12,15 +12,12 @@ "className": "DNSExtension", "install_requires": [ "dnspython3", - "dt" + "dt>=1.1.58", + "charset-normalizer<3" ], "activation": "Remote" }, "properties": [ - { - "key": "api_url", - "type": "String" - }, { "key": "api_token", "type": "Password" @@ -41,6 +38,11 @@ "key": "test_location", "type": "String" }, + { + "key": "test_timeout", + "type": "Integer", + "defaultValue": 2 + }, { "key": "log_level", "type": "Dropdown", @@ -75,22 +77,16 @@ "configUI": { "displayName": "DNS", "properties": [ - { - "key": "api_url", - "displayName": "Tenant URL", - "displayHint": "https://localhost:9999/e/ or https:///e/ or https://.live.dynatrace.com", - "displayOrder": 1 - }, { "key": "api_token", "displayName": "API Token", "displayHint": "Requires \"Create and read synthetic monitors, locations, and nodes\" permission", - "displayOrder": 2 + "displayOrder": 1 }, { "key": "test_name", "displayName": "(Optional) Synthetic monitor name", - "displayOrder": 5 + "displayOrder": 2 }, { "key": "dns_server", @@ -107,38 +103,44 @@ "key": "frequency", "displayName": "(Optional) Frequency", "displayHint": "Frequency in minutes, default: 15", - "displayOrder": 6 + "displayOrder": 5 + }, + { + "key": "test_timeout", + "displayName": "(Optional) Check Timeout", + "displayHint": "Check timeout in seconds, if empty default value is 2 seconds", + "displayOrder": 7 }, { "key": "test_location", "displayName": "(Optional) Location name", "displayHint": "Default: ActiveGate", - "displayOrder": 7 + "displayOrder": 8 }, { "key": "proxy_address", "displayName": "(Optional) Proxy Address", - "displayOrder": 8 + "displayOrder": 9 }, { "key": "proxy_username", "displayName": "(Optional) Proxy Username", - "displayOrder": 9 + "displayOrder": 10 }, { "key": "proxy_password", "displayName": "(Optional) Proxy Password", - "displayOrder": 10 + "displayOrder": 11 }, { "key": "log_level", "displayName": "Log level", - "displayOrder": 11 + "displayOrder": 12 }, { "key": "failure_count", "displayName": "Failure count", - "displayOrder": 12, + "displayOrder": 13, "displayHint": "Number of consecutive failures before reporting error" } ] diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/custom.remote.python.thirdparty_linux_commands.zip b/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/custom.remote.python.thirdparty_linux_commands.zip index 4902068..5f85061 100644 Binary files a/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/custom.remote.python.thirdparty_linux_commands.zip and b/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/custom.remote.python.thirdparty_linux_commands.zip differ diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/commands_imports/__init__.py b/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/commands_imports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/commands_imports/environment.py b/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/commands_imports/environment.py new file mode 100644 index 0000000..b4e9997 --- /dev/null +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/commands_imports/environment.py @@ -0,0 +1,42 @@ +import os +import re +from pathlib import Path +import logging + +log = logging.getLogger(__name__) + +tenant_regex = re.compile(r"\[(.*)\]") + + +def get_api_url() -> str: + endpoint = "" + environment = "" + + base_path = Path(__file__) + + # Only true if running from simulator + if "remotepluginmodule" not in str(base_path): + base_path = "/opt" if os.name != "nt" else "C:/Program Files" + base_path = Path(base_path) / "dynatrace" / "remotepluginmodule" / "agent" + + # Go up one level until the directory name is remotepluginmodule + while True: + base_path = base_path.parent + if base_path.name == "remotepluginmodule": + extensions_conf = base_path / "agent" / "conf" / "extensions.conf" + with open(extensions_conf, "r", errors="replace") as f: + for line in f: + if line.startswith("Server "): + endpoint = line.split(" ")[1].strip() + endpoint = endpoint.replace("/communication", "") + if line.startswith("Tenant "): + environment = line.split(" ")[1].strip() + if endpoint and environment: + api_url = f"{endpoint}/e/{environment}" + log.info(f"Found API URL: '{api_url}' in '{extensions_conf}'") + return api_url + else: + raise Exception(f"Could not find API URL after reading {extensions_conf}") + # End of the line + if base_path.parent == base_path: + raise Exception("Could not find config directory") diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/linux_commands.py b/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/linux_commands.py index ee0a5c0..c41a951 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/linux_commands.py +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/linux_commands.py @@ -6,14 +6,17 @@ from dynatrace import Dynatrace from dynatrace.environment_v1.synthetic_third_party import SYNTHETIC_EVENT_TYPE_OUTAGE +from commands_imports.environment import get_api_url -log = logging.getLogger(__name__) +DT_TIMEOUT_SECONDS = 10 ENGINE_NAME = "SSH" +log = logging.getLogger(__name__) class LinuxCommandsExtension(RemoteBasePlugin): def initialize(self, **kwargs): - self.dt_client = Dynatrace(self.config.get("api_url"), self.config.get("api_token"), log=log, proxies=self.build_proxy_url()) + self.api_url = get_api_url() + self.dt_client = Dynatrace(self.api_url, self.config.get("api_token"), log=log, proxies=self.build_proxy_url(), timeout=DT_TIMEOUT_SECONDS) self.executions = 0 self.failures_detected = 0 @@ -89,30 +92,33 @@ def query(self, **kwargs): else: self.failures_detected = 0 - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( - engine_name=ENGINE_NAME, - timestamp=datetime.now(), - location_id=location_id, - location_name=location, - test_id=self.activation.entity_id, - test_title=test_title, - step_title=step_title, - schedule_interval=frequency * 60, - success=success, - response_time=response_time, - edit_link=f"#settings/customextension;id={self.plugin_info.name}", - icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/linux_commands.png" - ) + try: + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( + engine_name=ENGINE_NAME, + timestamp=datetime.now(), + location_id=location_id, + location_name=location, + test_id=self.activation.entity_id, + test_title=test_title, + step_title=step_title, + schedule_interval=frequency * 60, + success=success, + response_time=response_time, + edit_link=f"#settings/customextension;id={self.plugin_info.name}", + icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/linux_commands.png" + ) - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( - test_id=self.activation.entity_id, - name=f"Command execution failed for {step_title}", - location_id=location_id, - timestamp=datetime.now(), - state="open" if not success else "resolved", - event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, - reason=reason, - engine_name=ENGINE_NAME, - ) + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( + test_id=self.activation.entity_id, + name=f"Command execution failed for {step_title}", + location_id=location_id, + timestamp=datetime.now(), + state="open" if not success else "resolved", + event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, + reason=reason, + engine_name=ENGINE_NAME, + ) + except Exception as e: + self.logger.error(f"Error reporting third party test results to {self.api_url}: '{e}'") self.executions += 1 \ No newline at end of file diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/plugin.json b/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/plugin.json index 9cf101d..bc8fe3a 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/plugin.json +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-linux-commands/src/plugin.json @@ -1,6 +1,6 @@ { "name": "custom.remote.python.thirdparty_linux_commands", - "version": "1.018", + "version": "1.021", "type": "python", "entity": "CUSTOM_DEVICE", "technologies": [ @@ -12,15 +12,13 @@ "className": "LinuxCommandsExtension", "install_requires": [ "paramiko", - "dt" + "dt>=1.1.58", + "charset-normalizer<3", + "cryptography<=37.0.4" ] }, "activation": "Remote", "properties": [ - { - "key": "api_url", - "type": "String" - }, { "key": "api_token", "type": "Password" @@ -143,12 +141,6 @@ "displayHint": "Custom test name. Defaults to \" ()\"", "displayOrder": 1 }, - { - "key": "api_url", - "displayName": "Dynatrace tenant URL", - "displayHint": "https://localhost:9999/e/ or https:///e/ or https://.live.dynatrace.com", - "displayOrder": 2 - }, { "key": "api_token", "displayName": "Dynatrace API Token", diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-ping/custom.remote.python.thirdparty_ping.zip b/third-party-synthetic/active-gate-extensions/extension-third-party-ping/custom.remote.python.thirdparty_ping.zip index 5ba92f7..1492491 100644 Binary files a/third-party-synthetic/active-gate-extensions/extension-third-party-ping/custom.remote.python.thirdparty_ping.zip and b/third-party-synthetic/active-gate-extensions/extension-third-party-ping/custom.remote.python.thirdparty_ping.zip differ diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-ping/src/ping_extension.py b/third-party-synthetic/active-gate-extensions/extension-third-party-ping/src/ping_extension.py index 481177e..0699761 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-ping/src/ping_extension.py +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-ping/src/ping_extension.py @@ -1,7 +1,8 @@ from datetime import datetime -import logging +import re from ruxit.api.base_plugin import RemoteBasePlugin +from ruxit.api.exceptions import ConfigException from dynatrace import Dynatrace from dynatrace.environment_v1.synthetic_third_party import SYNTHETIC_EVENT_TYPE_OUTAGE @@ -9,14 +10,25 @@ from ping_imports.environment import get_api_url +DT_TIMEOUT_SECONDS = 10 + class PingExtension(RemoteBasePlugin): def initialize(self, **kwargs): - api_url = get_api_url() - self.dt_client = Dynatrace(api_url, self.config.get("api_token"), log=self.logger, proxies=self.build_proxy_url()) + self.api_url = get_api_url() + self.dt_client = Dynatrace(self.api_url, self.config.get("api_token"), log=self.logger, proxies=self.build_proxy_url(), timeout=DT_TIMEOUT_SECONDS) self.executions = 0 self.failures_detected = 0 + valid_ip = r"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" + valid_hostname = r"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" + + target = self.config.get("test_target") + if not re.match(valid_ip, target) and not re.match(valid_hostname, target): + raise ConfigException(f"Invalid test_target: {target}, must be a valid IP or hostname") + + + def build_proxy_url(self): proxy_address = self.config.get("proxy_address") proxy_username = self.config.get("proxy_username") @@ -49,7 +61,7 @@ def query(self, **kwargs) -> None: frequency = int(self.config.get("frequency")) if self.config.get("frequency") else 15 if self.executions % frequency == 0: - timeout = self.config.get("test_timeout", 2) + timeout = int(self.config.get("test_timeout", 2)) or 2 ping_result = ping(target, timeout) self.logger.info(ping_result.as_dict()) @@ -65,31 +77,34 @@ def query(self, **kwargs) -> None: response_time = ping_result.rtt_avg or 0 - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( - engine_name="Ping", - timestamp=datetime.now(), - location_id=location_id, - location_name=location, - test_id=self.activation.entity_id, - test_title=test_title, - step_title=step_title, - schedule_interval=frequency * 60, - success=success, - response_time=response_time, - edit_link=f"#settings/customextension;id={self.plugin_info.name}", - icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-ping/ping.png", - ) - - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( - test_id=self.activation.entity_id, - name=f"Ping failed for {step_title}", - location_id=location_id, - timestamp=datetime.now(), - state="open" if not success else "resolved", - event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, - reason=f"Ping failed for {step_title}. Result: {str(ping_result.as_dict())}", - engine_name="Ping", - ) + try: + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( + engine_name="Ping", + timestamp=datetime.now(), + location_id=location_id, + location_name=location, + test_id=self.activation.entity_id, + test_title=test_title, + step_title=step_title, + schedule_interval=frequency * 60, + success=success, + response_time=response_time, + edit_link=f"#settings/customextension;id={self.plugin_info.name}", + icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-ping/ping.png", + ) + + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( + test_id=self.activation.entity_id, + name=f"Ping failed for {step_title}", + location_id=location_id, + timestamp=datetime.now(), + state="open" if not success else "resolved", + event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, + reason=f"Ping failed for {step_title}. Result: {str(ping_result.as_dict())}", + engine_name="Ping", + ) + except Exception as e: + self.logger.error(f"Error reporting third party test results to {self.api_url}: '{e}'") self.executions += 1 diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-ping/src/plugin.json b/third-party-synthetic/active-gate-extensions/extension-third-party-ping/src/plugin.json index 50b637a..0b69985 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-ping/src/plugin.json +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-ping/src/plugin.json @@ -1,6 +1,6 @@ { "name": "custom.remote.python.thirdparty_ping", - "version": "1.016", + "version": "1.021", "type": "python", "entity": "CUSTOM_DEVICE", "technologies": [ "ICMP" ], @@ -8,7 +8,7 @@ "source": { "package": "ping_extension", "className": "PingExtension", - "install_requires": [ "pingparsing", "dt" ], + "install_requires": [ "pingparsing", "dt", "charset-normalizer<3.0.0" ], "activation": "Remote" }, "properties": [ @@ -26,7 +26,8 @@ }, { "key": "test_timeout", - "type": "Integer" + "type": "Integer", + "defaultValue": 2 }, { "key": "test_location", diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-port/custom.remote.python.thirdparty_port.zip b/third-party-synthetic/active-gate-extensions/extension-third-party-port/custom.remote.python.thirdparty_port.zip index e0d0367..6193513 100644 Binary files a/third-party-synthetic/active-gate-extensions/extension-third-party-port/custom.remote.python.thirdparty_port.zip and b/third-party-synthetic/active-gate-extensions/extension-third-party-port/custom.remote.python.thirdparty_port.zip differ diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-port/src/plugin.json b/third-party-synthetic/active-gate-extensions/extension-third-party-port/src/plugin.json index 3949299..3ffa4d2 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-port/src/plugin.json +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-port/src/plugin.json @@ -1,6 +1,6 @@ { "name": "custom.remote.python.thirdparty_port", - "version": "1.016", + "version": "1.021", "metricGroup": "tech.Port", "type": "python", "entity": "CUSTOM_DEVICE", @@ -9,7 +9,7 @@ "source": { "package": "port_extension", "className": "PortExtension", - "install_requires": [ "dt", "pingparsing"], + "install_requires": [ "dt", "charset-normalizer<3.0.0"], "activation": "Remote" }, "properties": [ @@ -37,7 +37,8 @@ }, { "key": "test_timeout", - "type": "Integer" + "type": "Integer", + "defaultValue": 2 }, { "key": "test_location", diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-port/src/port_extension.py b/third-party-synthetic/active-gate-extensions/extension-third-party-port/src/port_extension.py index d1a43ed..8346a6e 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-port/src/port_extension.py +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-port/src/port_extension.py @@ -2,30 +2,39 @@ from typing import Dict import logging import socket +import re -from pingparsing import PingStats, PingParsing, PingTransmitter from ruxit.api.base_plugin import RemoteBasePlugin from datetime import datetime from dynatrace import Dynatrace from dynatrace.environment_v1.synthetic_third_party import SYNTHETIC_EVENT_TYPE_OUTAGE +from ruxit.api.exceptions import ConfigException from port_imports.environment import get_api_url +DT_TIMEOUT_SECONDS = 10 log = logging.getLogger(__name__) - class PortExtension(RemoteBasePlugin): def initialize(self, **kwargs): - api_url = get_api_url() - self.dt_client = Dynatrace(api_url, + self.api_url = get_api_url() + self.dt_client = Dynatrace(self.api_url, self.config.get("api_token"), log=self.logger, - proxies=self.build_proxy_url()) + proxies=self.build_proxy_url(), + timeout=DT_TIMEOUT_SECONDS) self.executions = 0 self.failures: Dict[str, int] = defaultdict(int) + valid_ip = r"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" + valid_hostname = r"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$" + + target = self.config.get("test_target_ip") + if not re.match(valid_ip, target) and not re.match(valid_hostname, target): + raise ConfigException(f"Invalid test_target: {target}, must be a valid IP or hostname") + def build_proxy_url(self): proxy_address = self.config.get("proxy_address") proxy_username = self.config.get("proxy_username") @@ -45,7 +54,7 @@ def build_proxy_url(self): def query(self, **kwargs) -> None: - log.setLevel(self.config.get("log_level")) + self.logger.setLevel(self.config.get("log_level")) target_ip = self.config.get("test_target_ip") @@ -65,14 +74,14 @@ def query(self, **kwargs) -> None: for i, port in enumerate(target_ports): if port: - timeout = self.config.get("test_timeout", 2) + timeout = int(self.config.get("test_timeout", 2)) or 2 step_success, step_response_time = test_port(target_ip, int(port), protocol=self.config.get("test_protocol", "TCP"), timeout=timeout) test_response_time += step_response_time - log.info(f"{target_ip}:{port} = {step_success}, {step_response_time}") + self.logger.info(f"{target_ip}:{port} = {step_success}, {step_response_time}") step_title = f"{target_ip}:{port}" if not step_success: @@ -80,7 +89,7 @@ def query(self, **kwargs) -> None: self.failures[step_title] += 1 if self.failures[step_title] < failure_count: - log.info(f"The result for {step_title} was: {step_success}. Attempt {self.failures[step_title]}/{failure_count}, not reporting yet") + self.logger.info(f"The result for {step_title} was: {step_success}. Attempt {self.failures[step_title]}/{failure_count}, not reporting yet") step_success = True else: self.failures[step_title] = 0 @@ -92,77 +101,93 @@ def query(self, **kwargs) -> None: self.dt_client.third_part_synthetic_tests.create_synthetic_test_step_result(i + 1, datetime.now(), step_response_time) ) - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( - engine_name="Port", - timestamp=datetime.now(), - location_id=location_id, - location_name=location, - test_id=f"{self.activation.entity_id}", - test_title=test_title, - schedule_interval=frequency * 60, - success=test_success, - response_time=test_response_time, - edit_link=f"#settings/customextension;id={self.plugin_info.name}", - detailed_steps=[test_step["step"] for test_step in test_steps], - detailed_step_results=test_step_results, - icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-port/port.png", - ) - - for step in test_steps: - event_name = f"Port check failed for {test_title} ({step['step'].title})" - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( - test_id=f"{self.activation.entity_id}", - name=event_name, - location_id=location_id, - timestamp=datetime.now(), - state="open" if not step["success"] else "resolved", - event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, - reason=event_name, + try: + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( engine_name="Port", + timestamp=datetime.now(), + location_id=location_id, + location_name=location, + test_id=f"{self.activation.entity_id}", + test_title=test_title, + schedule_interval=frequency * 60, + success=test_success, + response_time=test_response_time, + edit_link=f"#settings/customextension;id={self.plugin_info.name}", + detailed_steps=[test_step["step"] for test_step in test_steps], + detailed_step_results=test_step_results, + icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-port/port.png", ) + for step in test_steps: + event_name = f"Port check failed for {test_title} ({step['step'].title})" + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( + test_id=f"{self.activation.entity_id}", + name=event_name, + location_id=location_id, + timestamp=datetime.now(), + state="open" if not step["success"] else "resolved", + event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, + reason=event_name, + engine_name="Port", + ) + except Exception as e: + self.logger.error(f"Error reporting third party test results to {self.api_url}: '{e}'") + self.executions += 1 -def test_port(ip: str, port: int, protocol: str = "TCP", timeout: int = 2) -> (bool, int): - log.debug(f"Testing {ip}:{port} using protocol {protocol}") - start = datetime.now() - result = True +def udp_check(ip: str, port: int, timeout: int) -> bool: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + udp_recv_bufsize = 8192 + udp_send_payload = b"" + try: - socket_type = socket.SOCK_STREAM if protocol == "TCP" else socket.SOCK_DGRAM - sock = socket.socket(socket.AF_INET, socket_type) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.settimeout(timeout) sock.connect((ip, port)) + sock.send(udp_send_payload) + sock.settimeout(timeout) - if protocol == "UDP": - sock.sendall(b"") - data = sock.recv(1024) - log.debug(f"Received data: {data}") + try: + sock.recv(udp_recv_bufsize) + except socket.timeout: + pass + finally: + sock.settimeout(0) + local = sock.getsockname() + log.info(f"Connected to {ip}:{port} from {local[0]}:{local[1]}") + return True + except (OSError, socket.error) as error: + log.warning(f"Could not connect to {ip}:{port} - {error}") + return False + finally: sock.close() + +def tcp_check(ip: str, port: int, timeout: int) -> bool: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.settimeout(timeout) + sock.connect((ip, port)) + except socket.timeout: - if protocol == "UDP": - log.warning(f"The UDP test for {ip}:{port} timed out, checking if the host can be pinged before reporting") - ping_result = ping(ip, timeout) - log.info(f"Ping result to double check UDP: {ping_result.as_dict()}") - result = ping_result.packet_loss_rate is not None and ping_result.packet_loss_rate == 0 - else: - result = False + return False except Exception as ex: - log.error(f"Could not connect to {ip}:{port} with protocol {protocol} - {ex}") - result = False + log.error(f"Could not connect to {ip}:{port} - {ex}") + return False + finally: + sock.close() + return True - return result, int((datetime.now() - start).total_seconds() * 1000) +def test_port(ip: str, port: int, protocol: str = "TCP", timeout: int = 2) -> (bool, int): + log.debug(f"Testing {ip}:{port} using protocol {protocol}") + start = datetime.now() + if protocol == "UDP": + result = udp_check(ip, port, timeout) + else: + result = tcp_check(ip, port, timeout) -def ping(host: str, timeout: int) -> PingStats: - ping_parser = PingParsing() - transmitter = PingTransmitter() - transmitter.destination = host - transmitter.count = 1 - transmitter.timeout = timeout * 1000 - return ping_parser.parse(transmitter.ping()) + return result, int((datetime.now() - start).total_seconds() * 1000) def main(): diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/custom.remote.python.thirdparty_sftp.zip b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/custom.remote.python.sftp.zip similarity index 52% rename from third-party-synthetic/active-gate-extensions/extension-third-party-sftp/custom.remote.python.thirdparty_sftp.zip rename to third-party-synthetic/active-gate-extensions/extension-third-party-sftp/custom.remote.python.sftp.zip index 8f5e310..882a637 100644 Binary files a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/custom.remote.python.thirdparty_sftp.zip and b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/custom.remote.python.sftp.zip differ diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/plugin.json b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/plugin.json index 3566e27..2e121d0 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/plugin.json +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/plugin.json @@ -1,6 +1,6 @@ { - "name": "custom.remote.python.thirdparty_sftp", - "version": "1.015", + "name": "custom.remote.python.sftp", + "version": "1.021", "type": "python", "entity": "CUSTOM_DEVICE", "technologies": [ @@ -12,15 +12,13 @@ "className": "SFTPExtension", "install_requires": [ "paramiko", - "dt" + "dt>=1.1.58", + "charset-normalizer<3", + "cryptography<=37.0.4" ] }, "activation": "Remote", "properties": [ - { - "key": "api_url", - "type": "String" - }, { "key": "api_token", "type": "Password" @@ -116,12 +114,6 @@ "displayHint": "Custom test name. Defaults to \":\"", "displayOrder": 1 }, - { - "key": "api_url", - "displayName": "Dynatrace tenant URL", - "displayHint": "https://localhost:9999/e/ or https:///e/ or https://.live.dynatrace.com", - "displayOrder": 2 - }, { "key": "api_token", "displayName": "Dynatrace API Token", diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_extension.py b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_extension.py index 17b7169..c96adc1 100644 --- a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_extension.py +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_extension.py @@ -7,14 +7,18 @@ from dynatrace import Dynatrace from dynatrace.environment_v1.synthetic_third_party import SYNTHETIC_EVENT_TYPE_OUTAGE, SyntheticTestStep, SyntheticMonitorStepResult +from sftp_imports.environment import get_api_url log = logging.getLogger(__name__) -ENGINE_NAME = "SFTP" +DT_TIMEOUT_SECONDS = 10 + +ENGINE_NAME = "SFTP" class SFTPExtension(RemoteBasePlugin): def initialize(self, **kwargs): - self.dt_client = Dynatrace(self.config.get("api_url"), self.config.get("api_token"), log=log, proxies=self.build_proxy_url()) + self.api_url = get_api_url() + self.dt_client = Dynatrace(self.api_url, self.config.get("api_token"), log=log, proxies=self.build_proxy_url(), timeout=DT_TIMEOUT_SECONDS) self.executions = 0 self.failures_detected = 0 @@ -102,31 +106,35 @@ def query(self, **kwargs): else: self.failures_detected = 0 - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( - engine_name=ENGINE_NAME, - timestamp=datetime.now(), - location_id=location_id, - location_name=location, - test_id=self.activation.entity_id, - test_title=test_title, - schedule_interval=frequency * 60, - success=success, - response_time=test_response_time, - edit_link=f"#settings/customextension;id={self.plugin_info.name}", - icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/sftp.png", - detailed_steps=steps, - detailed_step_results=results - ) - - self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( - test_id=self.activation.entity_id, - name=f"SFTP Test failed for {test_title}", - location_id=location_id, - timestamp=datetime.now(), - state="open" if not success else "resolved", - event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, - reason=reason, - engine_name=ENGINE_NAME - ) + + try: + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test( + engine_name=ENGINE_NAME, + timestamp=datetime.now(), + location_id=location_id, + location_name=location, + test_id=self.activation.entity_id, + test_title=test_title, + schedule_interval=frequency * 60, + success=success, + response_time=test_response_time, + edit_link=f"#settings/customextension;id={self.plugin_info.name}", + icon_url="https://raw.githubusercontent.com/Dynatrace/dynatrace-api/master/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/sftp.png", + detailed_steps=steps, + detailed_step_results=results + ) + + self.dt_client.third_part_synthetic_tests.report_simple_thirdparty_synthetic_test_event( + test_id=self.activation.entity_id, + name=f"SFTP Test failed for {test_title}", + location_id=location_id, + timestamp=datetime.now(), + state="open" if not success else "resolved", + event_type=SYNTHETIC_EVENT_TYPE_OUTAGE, + reason=reason, + engine_name=ENGINE_NAME + ) + except Exception as e: + self.logger.error(f"Error reporting third party test results to {self.api_url}: '{e}'") self.executions += 1 \ No newline at end of file diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_imports/__init__.py b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_imports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_imports/environment.py b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_imports/environment.py new file mode 100644 index 0000000..b4e9997 --- /dev/null +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_imports/environment.py @@ -0,0 +1,42 @@ +import os +import re +from pathlib import Path +import logging + +log = logging.getLogger(__name__) + +tenant_regex = re.compile(r"\[(.*)\]") + + +def get_api_url() -> str: + endpoint = "" + environment = "" + + base_path = Path(__file__) + + # Only true if running from simulator + if "remotepluginmodule" not in str(base_path): + base_path = "/opt" if os.name != "nt" else "C:/Program Files" + base_path = Path(base_path) / "dynatrace" / "remotepluginmodule" / "agent" + + # Go up one level until the directory name is remotepluginmodule + while True: + base_path = base_path.parent + if base_path.name == "remotepluginmodule": + extensions_conf = base_path / "agent" / "conf" / "extensions.conf" + with open(extensions_conf, "r", errors="replace") as f: + for line in f: + if line.startswith("Server "): + endpoint = line.split(" ")[1].strip() + endpoint = endpoint.replace("/communication", "") + if line.startswith("Tenant "): + environment = line.split(" ")[1].strip() + if endpoint and environment: + api_url = f"{endpoint}/e/{environment}" + log.info(f"Found API URL: '{api_url}' in '{extensions_conf}'") + return api_url + else: + raise Exception(f"Could not find API URL after reading {extensions_conf}") + # End of the line + if base_path.parent == base_path: + raise Exception("Could not find config directory")