diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/README.md b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/README.md new file mode 100644 index 0000000..51fbc49 --- /dev/null +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/README.md @@ -0,0 +1,12 @@ +# Synthetic SFTP extension + +Each endpoint is a Synthetic Test representing the performance of an SFTP server + +### How to use + +1. Unzip `custom.remote.python.thirdparty_sftp.zip` to the `plugin_deployment` folder of the ActiveGate + - By default on Linux: `unzip custom.remote.python.thirdparty_linux_commands.zip -d /opt/dynatrace/remotepluginmodule/plugin_deployment` + - On windows: `C:\Program Files\dynatrace\remotepluginmodule\plugin_deployment` +2. Upload the extension to Dynatrace + - Settings > Monitored technologies > Custom extensions > Upload extension +3. Configure your endpoints on the same screen \ No newline at end of file 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.thirdparty_sftp.zip new file mode 100644 index 0000000..8f5e310 Binary files /dev/null and b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/custom.remote.python.thirdparty_sftp.zip differ diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/sftp.png b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/sftp.png new file mode 100644 index 0000000..e78a949 Binary files /dev/null and b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/sftp.png 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 new file mode 100644 index 0000000..3566e27 --- /dev/null +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/plugin.json @@ -0,0 +1,233 @@ +{ + "name": "custom.remote.python.thirdparty_sftp", + "version": "1.015", + "type": "python", + "entity": "CUSTOM_DEVICE", + "technologies": [ + "SFTP" + ], + "metricGroup": "tech.SFTP", + "source": { + "package": "sftp_extension", + "className": "SFTPExtension", + "install_requires": [ + "paramiko", + "dt" + ] + }, + "activation": "Remote", + "properties": [ + { + "key": "api_url", + "type": "String" + }, + { + "key": "api_token", + "type": "Password" + }, + { + "key": "test_name", + "type": "String" + }, + { + "key": "hostname", + "type": "String" + }, + { + "key": "username", + "type": "String" + }, + { + "key": "password", + "type": "Password" + }, + { + "key": "ssh_key_file", + "type": "String" + }, + { + "key": "ssh_key_passphrase", + "type": "Password" + }, + { + "key": "frequency", + "type": "String", + "defaultValue": "15" + }, + { + "key": "port", + "type": "String", + "defaultValue": "22" + }, + { + "key": "location", + "type": "String", + "defaultValue": "ActiveGate" + }, + { + "key": "remote_dir", + "type": "String" + }, + { + "key": "local_file", + "type": "String" + }, + { + "key": "test_read", + "type": "Boolean" + }, + { + "key": "test_put", + "type": "Boolean" + }, + { + "key": "log_level", + "type": "Dropdown", + "dropdownValues": [ + "INFO", + "DEBUG" + ], + "defaultValue": "INFO" + }, + { + "key": "proxy_address", + "type": "String" + }, + { + "key": "proxy_username", + "type": "String" + }, + { + "key": "proxy_password", + "type": "Password" + }, + { + "key": "failure_count", + "type": "Integer", + "defaultValue": 1 + } + ], + "configUI": { + "displayName": "SFTP", + "properties": [ + { + "key": "test_name", + "displayName": "(Optional) Synthetic test name", + "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", + "displayHint": "Requires \"Create and read synthetic monitors, locations, and nodes\" permission", + "displayOrder": 3 + }, + { + "key": "hostname", + "displayName": "Hostname", + "displayOrder": 4 + }, + { + "key": "port", + "displayName": "SSH Port", + "displayOrder": 5 + }, + { + "key": "username", + "displayName": "Username", + "displayOrder": 6 + }, + { + "key": "password", + "displayName": "Password", + "displayOrder": 7 + }, + { + "key": "ssh_key_file", + "displayName": "(Optional) Private key file", + "displayHint": "Path to a private key file to use for authentication", + "displayOrder": 8 + }, + { + "key": "ssh_key_passphrase", + "displayName": "(Optional) Private key passphrase", + "displayHint": "The private key passphrase if the key file uses one", + "displayOrder": 9 + }, + { + "key": "test_read", + "displayName": "Test read", + "displayHint": "Check if you want to test directory read", + "displayOrder": 10 + }, + { + "key": "test_put", + "displayName": "Test put", + "displayHint": "Check if you want to test uploading a file", + "displayOrder": 11 + }, + { + "key": "remote_dir", + "displayName": "Remote directory", + "displayHint": "Remote directory path used for reading and/or uploading a file to.", + "displayOrder": 12 + }, + { + "key": "local_file", + "displayName": "Local file for upload", + "displayHint": "A local file (under 100KBs) that can be uploaded remotely for testing.", + "displayOrder": 13 + }, + { + "key": "frequency", + "displayName": "Execution frequency", + "displayHint": "Command execution frequency (in minutes).", + "displayOrder": 14 + }, + { + "key": "location", + "displayName": "Synthetic test location", + "displayHint": "A custom name for the Synthetic Test location.", + "displayOrder": 15 + }, + { + "key": "failure_count", + "displayName": "Failure count", + "displayHint": "The number of consecutive failures required for an Outage to be reported", + "defaultValue": 1, + "displayOrder": 16 + }, + { + "key": "proxy_address", + "displayName": "(Optional) Proxy address", + "defaultValue": "Only needed if the Dynatrace tenant must be accessed through a proxy.", + "displayOrder": 17 + }, + { + "key": "proxy_username", + "displayName": "(Optional) Proxy username", + "defaultValue": "Only needed if the Dynatrace tenant must be accessed through a proxy.", + "displayOrder": 18 + }, + { + "key": "proxy_password", + "displayName": "(Optional) Proxy password", + "defaultValue": "Only needed if the Dynatrace tenant must be accessed through a proxy.", + "displayOrder": 19 + }, + { + "key": "log_level", + "displayName": "Log level", + "displayOrder": 20 + } + ] + }, + "metrics": [], + "ui": {} +} \ No newline at end of file diff --git a/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_client.py b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_client.py new file mode 100644 index 0000000..0528b50 --- /dev/null +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_client.py @@ -0,0 +1,91 @@ +import os +import time +import logging +import paramiko +from typing import Optional, Union + + +class SFTPClient: + def __init__( + self, + hostname: str, + port: int, + username: str, + password: Optional[str] = None, + key: Optional[str] = None, + passphrase: Optional[str] = None, + log: Optional[logging.Logger] = None + ): + self.hostname = hostname + self.port = port + self.username = username + self.password = password + self.key = key + self.passphrase = passphrase + self.log = log if log is not None else logging.getLogger(__name__) + self.connected = False + self.client: Optional[paramiko.SFTPClient] = None + self.transport: Optional[paramiko.Transport] = None + self.connect_time = time.time() + + def __enter__(self) -> "SFTPClient": + try: + self.transport = paramiko.Transport((self.hostname, self.port)) + if self.key is not None: + with open(file=self.key, mode="r") as f: + self.key = paramiko.RSAKey.from_private_key(f, self.passphrase) + self.transport.connect(None, self.username, self.password, self.key) + self.client = paramiko.SFTPClient.from_transport(self.transport) + except Exception as e: + self.log.exception(e) + self.connected = False + else: + self.connected = True + finally: + self.connect_time = int((time.time() - self.connect_time)*1000) + + return self + + def __exit__(self, e_type, e_value, e_trace) -> bool: + self.connected = False + if self.client is not None: + self.client.close() + if self.transport is not None: + self.transport.close() + + if e_type is not None: + self.log.exception(e_type, e_value, e_trace) + + return True + + def test_connect(self) -> Union[bool, str, int]: + if self.connected: + return True, "", self.connect_time + return False, f"Error connecting to host {self.hostname}", 0 + + def test_read(self, remote_dir: str) -> Union[bool, str, int]: + start = time.time() + try: + self.client.chdir(remote_dir) + self.client.listdir() + except Exception as e: + self.log.exception(e) + return False, f"Could not read directory {remote_dir}", 0 + else: + return True, "", int((time.time() - start)*1000) + + def test_put(self, local_file: str, remote_dir: str) -> Union[bool, str, int]: + start = time.time() + if os.path.getsize(local_file) > 100_000: + return False, "File size too large. Must be under 100KBs", 0 + + try: + filename = local_file.split(os.path.sep)[-1] + dest = f"{remote_dir}/{filename}" + self.log.info(f'Uploading local file "{local_file}" to remote destination "{dest}"') + self.client.put(local_file, dest) + except Exception as e: + self.log.exception(e) + return False, f"Error uploading file {local_file} to {dest}", 0 + else: + return True, "", int((time.time()-start)*1000) \ No newline at end of file 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 new file mode 100644 index 0000000..17b7169 --- /dev/null +++ b/third-party-synthetic/active-gate-extensions/extension-third-party-sftp/src/sftp_extension.py @@ -0,0 +1,132 @@ +import logging +from typing import List +from datetime import datetime + +from sftp_client import SFTPClient +from ruxit.api.base_plugin import RemoteBasePlugin +from dynatrace import Dynatrace +from dynatrace.environment_v1.synthetic_third_party import SYNTHETIC_EVENT_TYPE_OUTAGE, SyntheticTestStep, SyntheticMonitorStepResult + + +log = logging.getLogger(__name__) +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.executions = 0 + self.failures_detected = 0 + + def build_proxy_url(self): + proxy_address = self.config.get("proxy_address") + proxy_username = self.config.get("proxy_username") + proxy_password = self.config.get("proxy_password") + + if proxy_address: + protocol, address = proxy_address.split("://") + proxy_url = f"{protocol}://" + if proxy_username: + proxy_url += proxy_username + if proxy_password: + proxy_url += f":{proxy_password}" + proxy_url += f"@{address}" + return {"https": proxy_url} + + return {} + + def query(self, **kwargs): + log.setLevel(self.config.get("log_level")) + hostname = self.config.get("hostname").strip() + username = self.config.get("username") + port = int(self.config.get("port", 22)) + password = self.config.get("password") if self.config.get("password") else None + key = self.config.get("ssh_key_file") if self.config.get("ssh_key_file") else None + passphrase = self.config.get("ssh_key_passphrase") if self.config.get("ssh_key_passphrase") else None + test_read = self.config.get("test_read", False) + test_put = self.config.get("test_put", False) + local_file = self.config.get("local_file") if self.config.get("local_file") else None + remote_dir = self.config.get("remote_dir") if self.config.get("remote_dir") else None + + test_title = self.config.get("test_name") if self.config.get("test_name") else f"{hostname}:{port}" + location = self.config.get("location") if self.config.get("location") else "ActiveGate" + location_id = location.replace(" ", "_").lower() + frequency = int(self.config.get("frequency")) if self.config.get("frequency") else 15 + failure_count = self.config.get("failure_count", 1) + + if self.executions % frequency == 0: + steps: List[SyntheticTestStep] = [] + results: List[SyntheticMonitorStepResult] = [] + test_response_time = 0 + + with SFTPClient( + hostname=hostname, + port=port, + username=username, + password=password, + key=key, + passphrase=passphrase, + log=log + ) as client: + conn_success, reason, conn_time = client.test_connect() + log.info(f"Test: {test_title}, Step: Connect, success: {conn_success}, time: {conn_time}") + + success = conn_success + test_response_time += conn_time + steps.append(self.dt_client.third_part_synthetic_tests.create_synthetic_test_step(1, "SFTP Connect")) + results.append(self.dt_client.third_part_synthetic_tests.create_synthetic_test_step_result(1, datetime.now(), conn_time)) + + if conn_success and test_read: + read_success, reason, read_time = client.test_read(remote_dir) + log.info(f"Test: {test_title}, Step: Read, success: {read_success}, time: {read_time}") + + success = success and read_success + test_response_time += read_time + steps.append(self.dt_client.third_part_synthetic_tests.create_synthetic_test_step(2, "SFTP Read")) + results.append(self.dt_client.third_part_synthetic_tests.create_synthetic_test_step_result(2, datetime.now(), read_time)) + + if conn_success and test_put: + put_success, reason, put_time = client.test_put(local_file, remote_dir) + log.info(f"Test: {test_title}, Step: Put, success: {put_success}, time: {put_time}") + + success = success and put_success + test_response_time += put_time + steps.append(self.dt_client.third_part_synthetic_tests.create_synthetic_test_step(3, "SFTP Put")) + results.append(self.dt_client.third_part_synthetic_tests.create_synthetic_test_step_result(3, datetime.now(), put_time)) + + if not success: + self.failures_detected += 1 + if self.failures_detected < failure_count: + log.info(f"Success: {success}. Attempt {self.failures_detected}/{failure_count}. Not reporting yet") + success = True + 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 + ) + + self.executions += 1 \ No newline at end of file