feat: add TunnelConfig dataclass and YAML I/O

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 11:31:19 +08:00
parent 113cc994b1
commit 53133ccb67
3 changed files with 151 additions and 0 deletions

71
src/autossh_mgr/config.py Normal file
View File

@@ -0,0 +1,71 @@
import os
from dataclasses import dataclass, asdict, field
from pathlib import Path
from typing import Optional
import click
import yaml
def get_config_dir() -> Path:
env = os.environ.get("AUTOSSH_MGR_CONFIG_DIR")
if env:
return Path(env)
return Path.home() / ".config" / "autossh-mgr"
@dataclass
class TunnelConfig:
name: str
host: str
user: str
local_port: int
remote_port: int
port: int = 22
identity_file: str = "~/.ssh/id_ed25519"
local_host: str = "127.0.0.1"
remote_host: str = "0.0.0.0"
ssh_options: str = ""
_DEFAULTS = TunnelConfig(
name="", host="", user="", local_port=0, remote_port=0
)
def ensure_dirs(config_dir: Path) -> None:
config_dir.mkdir(parents=True, exist_ok=True)
(config_dir / "pids").mkdir(exist_ok=True)
(config_dir / "logs").mkdir(exist_ok=True)
tunnels_file = config_dir / "tunnels.yaml"
if not tunnels_file.exists():
tunnels_file.write_text("tunnels: []\n")
def load_tunnels(config_dir: Path) -> list[TunnelConfig]:
tunnels_file = config_dir / "tunnels.yaml"
if not tunnels_file.exists():
return []
data = yaml.safe_load(tunnels_file.read_text()) or {}
return [TunnelConfig(**entry) for entry in data.get("tunnels", [])]
def save_tunnels(config_dir: Path, tunnels: list[TunnelConfig]) -> None:
rows = []
for t in tunnels:
d = asdict(t)
# Omit optional fields that are at their default values
for key in ("port", "identity_file", "local_host", "remote_host", "ssh_options"):
if d[key] == getattr(_DEFAULTS, key):
del d[key]
rows.append(d)
tmp = config_dir / "tunnels.yaml.tmp"
tmp.write_text(yaml.dump({"tunnels": rows}, default_flow_style=False))
tmp.replace(config_dir / "tunnels.yaml")
def get_tunnel(config_dir: Path, name: str) -> TunnelConfig:
for t in load_tunnels(config_dir):
if t.name == name:
return t
raise click.ClickException(f"No tunnel named '{name}'")