blob: 3bc064094060e97e9acfffb92e3621a71dbef1e1 [file]
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())