| from __future__ import annotations |
| |
| import argparse |
| import os |
| from pathlib import Path |
| import shlex |
| import subprocess |
| import sys |
| from collections.abc import Mapping, Sequence |
| |
| from iotdb_mcp_server.target_registry import IoTDBTarget, IoTDBTargetRegistry |
| |
| _TARGET_CONTROLLED_OPTIONS = { |
| "-h", |
| "--host", |
| "-p", |
| "--port", |
| "-u", |
| "--user", |
| "--username", |
| "-pw", |
| "--password", |
| "-sql_dialect", |
| "--sql-dialect", |
| "-db", |
| "--database", |
| } |
| _DATABASE_OPTION_CLI_NAMES = {"import-data.sh", "import-data.bat"} |
| |
| |
| def _target_cli_path( |
| target: IoTDBTarget, |
| explicit_cli: str | None, |
| env: Mapping[str, str], |
| ) -> Path: |
| configured = explicit_cli or env.get("TIMESEEK_IOTDB_CLI", "") |
| if configured: |
| return Path(configured).expanduser() |
| |
| iotdb_home = target.iotdb_home or env.get("TIMESEEK_IOTDB_HOME", "") |
| if not iotdb_home: |
| raise ValueError( |
| "IoTDB CLI is not configured. Set target.iotdb_home, " |
| "TIMESEEK_IOTDB_HOME, or pass --cli." |
| ) |
| return Path(iotdb_home).expanduser() / "sbin" / "start-cli.sh" |
| |
| |
| def _validate_passthrough_args(cli_args: Sequence[str]) -> None: |
| for argument in cli_args: |
| option = argument.split("=", 1)[0] |
| if option in _TARGET_CONTROLLED_OPTIONS: |
| raise ValueError( |
| f"Do not pass {option} to iotdb-target-cli. Endpoint, credentials, " |
| "and dialect are loaded from the verified target; database options " |
| "are injected only for CLI tools that support them." |
| ) |
| |
| |
| def cli_supports_database_option(cli_path: str | Path) -> bool: |
| return Path(cli_path).name.lower() in _DATABASE_OPTION_CLI_NAMES |
| |
| |
| def build_target_cli_command( |
| target: IoTDBTarget, |
| cli_path: str | Path, |
| cli_args: Sequence[str] = (), |
| ) -> list[str]: |
| passthrough = list(cli_args) |
| if passthrough[:1] == ["--"]: |
| passthrough = passthrough[1:] |
| _validate_passthrough_args(passthrough) |
| |
| command = [ |
| str(cli_path), |
| "-h", |
| target.host, |
| "-p", |
| str(target.port), |
| "-u", |
| target.user, |
| ] |
| if target.password: |
| command.extend(["-pw", target.password]) |
| command.extend(["-sql_dialect", target.sql_dialect]) |
| if ( |
| target.sql_dialect == "table" |
| and target.database |
| and cli_supports_database_option(cli_path) |
| ): |
| command.extend(["-db", target.database]) |
| command.extend(passthrough) |
| return command |
| |
| |
| def redact_target_cli_command(command: Sequence[str]) -> list[str]: |
| redacted = list(command) |
| try: |
| password_index = redacted.index("-pw") + 1 |
| except ValueError: |
| return redacted |
| if password_index < len(redacted): |
| redacted[password_index] = "<redacted>" |
| return redacted |
| |
| |
| def _parser() -> argparse.ArgumentParser: |
| parser = argparse.ArgumentParser( |
| description=( |
| "Run the IoTDB Java CLI with endpoint and credentials loaded from a " |
| "verified TimeSeek target." |
| ) |
| ) |
| parser.add_argument( |
| "--targets-file", |
| help="Target registry JSON. Defaults to TIMESEEK_IOTDB_TARGETS_FILE.", |
| ) |
| parser.add_argument( |
| "--target-id", |
| help="Verified target id. Defaults to the registry default target.", |
| ) |
| parser.add_argument( |
| "--cli", |
| help=( |
| "Path to start-cli.sh. Defaults to TIMESEEK_IOTDB_CLI or " |
| "<target.iotdb_home>/sbin/start-cli.sh." |
| ), |
| ) |
| parser.add_argument( |
| "--print-command", |
| action="store_true", |
| help="Print a shell-escaped command with the password redacted, then exit.", |
| ) |
| parser.add_argument( |
| "cli_args", |
| nargs=argparse.REMAINDER, |
| help="Arguments passed to start-cli.sh after an optional -- separator.", |
| ) |
| return parser |
| |
| |
| def _run(argv: Sequence[str] | None = None) -> int: |
| args = _parser().parse_args(argv) |
| env = os.environ |
| targets_file = args.targets_file or env.get("TIMESEEK_IOTDB_TARGETS_FILE") |
| registry = IoTDBTargetRegistry.from_env( |
| env=env, |
| targets_file=targets_file, |
| ) |
| target = registry.resolve(args.target_id) |
| cli_path = _target_cli_path(target, args.cli, env) |
| if not cli_path.is_file(): |
| raise FileNotFoundError(f"IoTDB CLI does not exist: {cli_path}") |
| command = build_target_cli_command(target, cli_path, args.cli_args) |
| |
| if args.print_command: |
| print(shlex.join(redact_target_cli_command(command))) |
| return 0 |
| |
| completed = subprocess.run(command, check=False) |
| return completed.returncode |
| |
| |
| def main(argv: Sequence[str] | None = None) -> int: |
| try: |
| return _run(argv) |
| except (OSError, KeyError, ValueError) as exc: |
| print(f"iotdb-target-cli: {exc}", file=sys.stderr) |
| return 2 |
| |
| |
| if __name__ == "__main__": |
| raise SystemExit(main()) |