From 5799a791c2b5ca26fbb9a43c2538debd8b8f2f5d Mon Sep 17 00:00:00 2001 From: Amir Husayn Panahifar Date: Sat, 12 Sep 2026 13:47:32 +0330 Subject: [PATCH] docs: add readme, changelog, contributing and man page --- CHANGELOG.md | 62 +++++++++ CONTRIBUTING.md | 66 +++++++++ README.md | 359 ++++++++++++++++++++++++++++++++++++++++++++++++ man/sshctl.1 | 200 +++++++++++++++++++++++++++ 4 files changed, 687 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 man/sshctl.1 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..005c4f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/), +and this project adheres to [Semantic Versioning](https://semver.org/). + +## [0.1.1] - 2026-09-12 + +### Fixed + +- **Parser**: directory `Include` targets (e.g. `conf.d/`) no longer crash validation with `IsADirectoryError`; `parse_file` converts read errors to `ParseError` +- **Init**: generated `Include conf.d/` now writes a proper glob (`conf.d/*.conf`) +- **Config**: `config set` no longer recurses infinitely when no `Host *` block exists (now prepends one) +- **Config**: `validate_options`/`heal` now detect lowercase option typos (`hostname` → `HostName`) +- **Jump**: comments with leading whitespace in `.internal_ssids` are now filtered correctly +- **CLI**: fixed undefined `HostBlock` name in host edit and broken `jump_auto` indentation +- **Types**: mypy strict clean across all 45 source/test files (jump, setup, configops, events) + +### Added + +- **Packaging**: Arch Linux `PKGBUILD` (`linux/arch/`) +- **Tests**: coverage suites for events, logutil, cleanup, configops, jump (161 tests) +- **Metadata**: repo URL unified to `https://src.panahifar.ir/ahp/sshctl` + +### Changed + +- Version bumped to 0.1.1; coverage gate aligned to 45% (CI and Makefile) + +## [0.1.0] - 2026-07-16 + +### Added + +- **Core**: SSH config parser with `Include` resolution, glob support, continuation lines +- **Core**: SSH config generator with deterministic option ordering +- **Core**: Host resolver via `ssh -G` subprocess +- **Core**: Config validator with structural validation +- **Operations**: Async DNS/TCP/SSH connectivity checker with semaphore concurrency +- **Operations**: Parallel SSH command executor +- **Export**: CSV inventory export +- **Export**: Ansible inventory generation from `conf.d/` directory tree +- **Export**: SSH directory permission fix and check +- **CLI**: `init` — initialize SSH config directory structure +- **CLI**: `host list`, `host show`, `host add` — host alias management +- **CLI**: `check` — connectivity check across hosts +- **CLI**: `exec` — parallel command execution +- **CLI**: `csv` — CSV inventory export +- **CLI**: `chmod` — permission fix/check +- **CLI**: `ansible` — Ansible inventory generation +- **CLI**: `validate` — config validation +- **CLI**: `tree` — config directory tree viewer +- **CLI**: `diff` — compare generated vs current config +- **CLI**: `backup` / `restore` — SSH directory backup and restore +- **CLI**: `install-completion` — shell completion installation +- **Architecture**: Modular subpackage layout (cli/, core/, config/, operations/, export/) +- **Infrastructure**: GitHub Actions CI (test + build wheel + deb) +- **Infrastructure**: pre-commit hooks (ruff, mypy, linting) +- **Infrastructure**: Debian packaging (`debian/`) +- **Infrastructure**: Makefile with test/lint/build targets +- **Infrastructure**: Coverage config (80% threshold) +- **Config**: Pydantic-settings with YAML and env variable support +- **Models**: ZonePath hierarchical addressing (universe/sector/cluster/zone/vlan) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c788edb --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,66 @@ +# Contributing + +## Development Setup + +```bash +git clone https://src.panahifar.ir/ahp/sshctl.git +cd sshctl +python -m venv .venv +source .venv/bin/activate +make dev +``` + +## Code Quality + +Before submitting a PR, run the full check suite: + +```bash +make check +``` + +This runs lint, format-check, typecheck, and tests in sequence. + +## Code Conventions + +- **Python 3.13+** — use modern typing (`str | None`, `list[str]`, etc.) +- **`from __future__ import annotations`** — required in every file +- **`@dataclass`** — preferred for data models +- **Imports** — lazy imports in CLI layer, top-level imports in domain modules +- **Async** — `asyncio` with `Semaphore` for all network operations +- **Testing** — pytest with `tmp_path` for files, `pytest.raises` for exceptions +- **Type annotations** — required on all public functions + +## Project Structure + +``` +src/sshctl/ + cli/ CLI interface (app.py) + core/ Shared foundation (models, exceptions, config, logging) + config/ SSH config lifecycle (parser, generator, resolver, validator) + operations/ Active operations (checker, executor) + export/ Export/integration (exporter, inventory, permissions) +``` + +## Pull Request Process + +1. Ensure `make check` passes +2. Add tests for new functionality +3. Update CHANGELOG.md +4. Update README.md if user-facing changes + +## Release Process + +```bash +# Update version in src/sshctl/core/_version.py +# Update CHANGELOG.md +git commit -m "Release v0.x.y" +git tag v0.x.y +git push origin main --tags +# CI builds and publishes wheel + deb +``` + +Or manually: + +```bash +make release +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..d36cb58 --- /dev/null +++ b/README.md @@ -0,0 +1,359 @@ +# sshctl + +SSH configuration management toolkit — parse, validate, generate, and manage SSH configs at scale. + +[![Python](https://img.shields.io/badge/python-3.13+-blue.svg)]() +[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) + +--- + +## Features + +- **Parse** — Full SSH config parser supporting `Host`, `Include`, glob patterns, and continuation lines +- **Generate** — Programmatic SSH config generation with deterministic option ordering +- **Validate** — Validate SSH config structure, check required options and missing includes +- **Check** — Parallel DNS/TCP/SSH connectivity checking across all hosts +- **Exec** — Run commands concurrently across all hosts via SSH +- **Export** — Export host inventory to CSV +- **Ansible** — Generate Ansible inventory from your SSH config directory tree +- **Permissions** — Fix or verify SSH directory permissions (0700 dirs, 0600 keys, 0644 public keys) +- **CLI** — Full-featured `typer` CLI with rich output (tables, trees, status) + +--- + +## Installation + +### pip (wheel) + +```bash +pip install sshctl # once published to PyPI +# or install the built wheel directly from a release +pip install ./sshctl-0.1.1-py3-none-any.whl +``` + +### dpkg (Debian/Ubuntu) + +```bash +# Download from latest release +wget https://src.panahifar.ir/ahp/sshctl/releases/download/v0.1.1/sshctl_0.1.1-1_all.deb +sudo dpkg -i sshctl_0.1.1-1_all.deb +``` + +### From source + +```bash +git clone https://src.panahifar.ir/ahp/sshctl.git +cd sshctl +pip install -e . +``` + +Download the latest `.whl` or `.deb` from the [releases page](https://src.panahifar.ir/ahp/sshctl/releases). + +--- + +## Quick Start + +```bash +# Initialize SSH config directory structure +sshctl init + +# List all hosts +sshctl host list + +# Show a specific host +sshctl host show server1 + +# Validate config +sshctl validate + +# Check host connectivity +sshctl check + +# Run a command across all hosts in parallel +sshctl exec "uptime" + +# Export inventory as CSV +sshctl csv --output hosts.csv + +# Generate Ansible inventory +sshctl ansible + +# Fix SSH directory permissions +sshctl chmod + +# Only check permissions (no fixes) +sshctl chmod --check + +# Show conf.d directory tree +sshctl tree +``` + +--- + +## Directory Structure + +``` +~/.ssh/ +├── config # Main SSH config (Include conf.d/) +├── conf.d/ # Modular host configurations +│ ├── internal/ # Internal infrastructure universe +│ │ ├── AZ/ # Sector (e.g., region) +│ │ │ └── cluster1/ +│ │ │ └── 8xx-serverfarm/ +│ │ │ └── 82-k8s/ +│ │ │ └── hosts.conf +│ │ └── ... +│ └── external/ # External/dedicated universe +│ └── ... +├── control.d/ # SSH control master sockets +├── keys.d/ # Private keys (auto-protected by sshctl chmod) +├── keys.pub/ # Public keys +├── report/ # Generated reports +└── inventory.d/ # Generated Ansible inventory + └── hosts.yml +``` + +### ZonePath Convention + +The `conf.d/` hierarchy follows a **ZonePath** addressing model: + +``` +conf.d/{universe}/{sector}/{cluster}/{zone}/{vlan} +``` + +| Level | Description | Example | +|----------|--------------------------|------------------------| +| universe | `internal` or `external` | `internal` | +| sector | Geographic/segment | `AZ` | +| cluster | Data center / DC | `cluster1` | +| zone | Server farm / role | `8xx-serverfarm` | +| vlan | Network segment | `82-k8s` | + +--- + +## CLI Reference + +### Global Options + +| Flag | Description | +|------|-------------| +| `-V`, `--version` | Show version | +| `-v`, `--verbose` | Verbose logging | +| `-c`, `--config` | Path to SSH config file | + +### Commands + +| Command | Description | +|---------|-------------| +| `init` | Initialize SSH config directory structure | +| `host list` | List all host aliases | +| `host show` | Show details for a specific host | +| `host add` | Add a new host to the SSH config | +| `check` | Check connectivity to all hosts | +| `exec` | Run a command in parallel across hosts | +| `csv` | Export host inventory to CSV | +| `chmod` | Fix or check SSH directory permissions | +| `validate` | Validate SSH config structure | +| `diff` | Show differences between generated and current config | +| `backup` | Create timestamped backup of SSH config directory | +| `restore ` | Restore SSH config from a backup | +| `ansible` | Generate Ansible inventory from SSH config | +| `tree` | Show the conf.d directory tree | +| `install-completion` | Install shell completions (bash/zsh/fish) | + +--- + +## Python API + +```python +from pathlib import Path +from sshctl.parser import parse_file, resolve_includes +from sshctl.models import HostBlock, SshConfig +from sshctl.generator import generate_config_text, write_config_file +from sshctl.validator import validate_config +from sshctl.resolver import resolve_host, resolve_all_hosts +from sshctl.exporter import export_csv +from sshctl.permissions import fix_ssh_permissions, check_permissions +from sshctl.inventory import generate_inventory + +# Parse an SSH config +config = parse_file(Path("~/.ssh/config").expanduser()) + +# Resolve includes recursively +full_config = resolve_includes(Path("~/.ssh/config")) + +# Find hosts +block = config.find("server1") +blocks = config.find_all("web*") + +# Validate +result = validate_config(Path("~/.ssh/config")) +if not result.passed: + for issue in result.issues: + print(f"[{issue.severity}] {issue.message}") + +# Resolve a host (uses ssh -G) +resolved = resolve_host("server1", Path("~/.ssh/config")) + +# Generate config text +text = generate_config_text(full_config) + +# Generate Ansible inventory +inventory = generate_inventory(Path("~/.ssh")) + +# Fix permissions +counts = fix_ssh_permissions(Path("~/.ssh")) +``` + +--- + +## Configuration + +sshctl can be configured via YAML or environment variables. + +### YAML (`~/.ssh/sshctl.yaml`) + +```yaml +ssh_dir: ~/.ssh +config_file: config +verbose: false +timeout: 10 +``` + +### Environment Variables + +All settings can be overridden with `SSHCTL_` prefix: + +```bash +export SSHCTL_VERBOSE=true +export SSHCTL_TIMEOUT=30 +``` + +--- + +## Development + +### Setup + +```bash +git clone https://src.panahifar.ir/ahp/sshctl.git +cd sshctl +python -m venv .venv +source .venv/bin/activate +pip install -e ".[dev]" +``` + +### Run Tests + +```bash +pytest +``` + +### Lint & Type Check + +```bash +ruff check src/ tests/ +ruff format --check src/ tests/ +mypy src/ tests/ +``` + +--- + +## Project Structure + +``` +sshctl/ +├── src/ +│ └── sshctl/ +│ ├── __init__.py # Package init, re-exports from core +│ ├── __main__.py # Entry point for python -m sshctl +│ │ +│ ├── cli/ # CLI layer (user-facing) +│ │ ├── __init__.py +│ │ ├── app.py # Typer CLI application +│ │ └── __main__.py # Module entry point +│ │ +│ ├── core/ # Core domain (shared foundation) +│ │ ├── __init__.py # Exports __version__ +│ │ ├── _version.py # Version string +│ │ ├── config.py # Pydantic settings (SshctlConfig) +│ │ ├── exceptions.py # Exception hierarchy +│ │ ├── logutil.py # Structured logging (structlog) +│ │ └── models.py # Data models (HostBlock, SshConfig, ZonePath) +│ │ +│ ├── config/ # SSH config lifecycle +│ │ ├── __init__.py +│ │ ├── parser.py # SSH config parser + include resolution +│ │ ├── generator.py # SSH config text generation +│ │ ├── resolver.py # Host resolution via ssh -G +│ │ └── validator.py # Config validation +│ │ +│ ├── operations/ # Active operations +│ │ ├── __init__.py +│ │ ├── checker.py # DNS/TCP/SSH connectivity checks +│ │ └── executor.py # Parallel SSH command execution +│ │ +│ └── export/ # Export/integration +│ ├── __init__.py +│ ├── exporter.py # CSV export +│ ├── inventory.py # Ansible inventory generation +│ └── permissions.py # SSH directory permissions +│ +├── tests/ +│ ├── conftest.py # Pytest fixtures +│ ├── test_config.py +│ ├── test_exceptions.py +│ ├── test_exporter.py +│ ├── test_generator.py +│ ├── test_inventory.py +│ ├── test_models.py +│ ├── test_parser.py +│ ├── test_permissions.py +│ └── test_validator.py +│ +├── pyproject.toml # Build config, ruff, mypy, pytest +├── .editorconfig +├── .gitignore +├── LICENSE +└── README.md +``` + +--- + +## Architecture + +sshctl follows a **modular layered architecture** organized into subpackages: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ cli/ (app.py) │ +│ Typer CLI, Rich output, orchestration │ +└──────────────────────┬──────────────────────────────────────┘ + │ + ┌────────────┼────────────────┬────────────────┐ + ▼ ▼ ▼ ▼ + ┌──────────┐ ┌───────────┐ ┌──────────────┐ ┌──────────────┐ + │ config/ │ │operations │ │ export/ │ │ core/ │ + │ │ │ │ │ │ │ │ + │ parser │ │ checker │ │ exporter │ │ models │ + │ generator│ │ executor │ │ inventory │ │ exceptions │ + │ resolver │ │ │ │ permissions │ │ config │ + │ validator│ │ │ │ │ │ logutil │ + └──────────┘ └───────────┘ └──────────────┘ └──────────────┘ + ▲ + │ + (shared foundation) +``` + +**Key design decisions:** + +- **Models first** — `HostBlock`, `SshConfig`, `ZonePath` are plain dataclasses; the entire pipeline operates on them +- **Parser is lossy by design** — it extracts enough structure for management, not a perfect SSH config round-trip +- **Resolver delegates to `ssh -G`** — rather than reimplementing SSH's full inheritance logic, sshctl shells out for the canonical resolved view +- **Async everywhere** — checker and executor use `asyncio` with semaphore-controlled concurrency + +--- + +## License + +MIT — see [LICENSE](LICENSE) diff --git a/man/sshctl.1 b/man/sshctl.1 new file mode 100644 index 0000000..e9cff9a --- /dev/null +++ b/man/sshctl.1 @@ -0,0 +1,200 @@ +.TH SSHCTL 1 "2026-09-12" "sshctl v0.1.1" "SSH Configuration Management" +.SH NAME +sshctl \- SSH configuration management toolkit +.SH SYNOPSIS +.B sshctl +[\fIoptions\fR] \fIcommand\fR [\fIargs\fR] +.SH DESCRIPTION +sshctl is a CLI toolkit for managing SSH configurations, including host definitions, +proxy jumps, permissions, connectivity checks, and shell completions. +.SH OPTIONS +.TP +.B \-V, \-\-version +Show version. +.TP +.B \-v, \-\-verbose +Verbose output. +.TP +.B \-c, \-\-config \fIPATH\fR +Path to ssh_config file (default: ~/.ssh/config). +.TP +.B \-\-install-completion +Install shell completion for the current shell. +.TP +.B \-\-show-completion +Show shell completion script. +.TP +.B \-\-help +Show help message. +.SH COMMANDS +.SS Core +.TP +.B init +Initialize an SSH config directory structure. +.TP +.B validate +Validate SSH config structure and option correctness. +.TP +.B diff +Show differences between generated and current SSH config. +.TP +.B tree +Show the conf.d directory tree with host counts. +.TP +.B backup +Create a timestamped backup of the SSH config directory. +.TP +.B restore \fIPATH\fR +Restore SSH config from a backup. +.TP +.B cleanup [\-n] +Remove stale SSH control master sockets. Use \-n for dry-run. +.TP +.B log [\-n \fICOUNT\fR] [\-f] +Show recent sshctl activity log. Use \-f to follow (tail mode). +.TP +.B check [\-p \fIPATTERN\fR] [\-t \fISECS\fR] [\-c \fIMAX\fR] +Check connectivity to all hosts in parallel. +.TP +.B exec \fICOMMAND\fR [\-p \fIPATTERN\fR] [\-t \fISECS\fR] +Run a command in parallel across hosts. +.TP +.B csv [\-o \fIFILE\fR] +Export host inventory to CSV. +.TP +.B ansible [\-o \fIFILE\fR] [\-\-group-by \fIFIELD\fR] +Generate Ansible inventory from SSH config. +.TP +.B chmod [\-\-fix] +Check (and optionally fix) SSH directory permissions. +.SS Config Management +.TP +.B config show +Show all global Host * options. +.TP +.B config set \fIKEY\fR \fIVALUE\fR +Set an option in the Host * block. +.TP +.B config unset \fIKEY\fR +Remove an option from the Host * block. +.SS Host Management +.TP +.B host list +List all host aliases with HostName and ProxyJump. +.TP +.B host show \fIALIAS\fR +Show options for a specific host. +.TP +.B host add \fIALIAS\fR \fIHOSTNAME\fR [\-u \fIUSER\fR] [\-p \fIPORT\fR] [\-j \fIJUMP\fR] +Add a new host alias. +.TP +.B host edit \fIALIAS\fR \-\-option \fIOPT\fR \-\-value \fIVAL\fR +Set an option on an existing host. +.TP +.B host remove \fIALIAS\fR [\-f] +Remove a host from its config file (with confirmation). +.SS ProxyJump +.TP +.B jump on +Restore all saved ProxyJump directives. +.TP +.B jump off +Save and remove all ProxyJump lines from conf.d files. +.TP +.B jump status +List all hosts with ProxyJump and show saved state. +.TP +.B jump auto [\-\-subnet] [\-\-no-subnet] +Auto-detect network environment: +- Probes jump gateways via TCP (respects HostName/Port from SSH config) +- Falls back to SSID matching against known internal networks +- Enables or disables ProxyJump accordingly. +.TP +.B jump networks [list|add|remove] [\-\-ssid \fIPATTERN\fR] +Manage internal SSID patterns for auto-detection (defaults: MobinNet*, Mobin*). +.SS Setup +.TP +.B setup completions [\-\-shell \fIbash|zsh|fish\fR] +Install shell completions. +.TP +.B setup agent [start|stop|restart|status] +Manage the ssh-agent systemd user service. +.TP +.B setup timer [cleanup|check] [\-a enable|disable|status] +Manage systemd timers. +.TP +.B setup git +Initialize git tracking in ~/.ssh. +.TP +.B setup init +Full setup: git + agent + timers + completions. +.SS Diagnostics +.TP +.B heal [\-\-dry-run] +Detect and fix config issues (typos, invalid options). +.SH FILES +.TP +.I ~/.ssh/config +Main SSH configuration file. +.TP +.I ~/.ssh/conf.d/ +Directory containing host definition files. +.TP +.I ~/.ssh/control.d/ +SSH control master socket directory. +.TP +.I ~/.ssh/keys.d/ +SSH key files. +.TP +.I ~/.ssh/scripts/ +Helper scripts and systemd unit files. +.TP +.I ~/.ssh/report/events.ndjson +Activity event log (NDJSON format). +.TP +.I ~/.ssh/.proxyjump_save +Saved ProxyJump state (temporary, created by \fBjump off\fR). +.TP +.I ~/.ssh/.internal_ssids +Internal SSID patterns for auto-detect. +.SH ENVIRONMENT +.TP +.B SSH_AUTH_SOCK +SSH agent socket path. Set to \fI$XDG_RUNTIME_DIR/ssh-agent.socket\fR when using +the built-in systemd ssh-agent service. +.SH EXAMPLES +.TP +Initialize a new SSH config directory: + sshctl init +.TP +List all hosts: + sshctl host list +.TP +Show host details: + sshctl host show myserver +.TP +Disable all ProxyJump directives (internal network): + sshctl jump off +.TP +Auto-detect network and toggle ProxyJump: + sshctl jump auto +.TP +Add current Wi-Fi as internal network: + sshctl jump networks add +.TP +Check all hosts connectivity: + sshctl check +.TP +View activity log: + sshctl log \-n 20 +.TP +Validate and fix config issues: + sshctl heal +.SH BUGS +Report issues at https://git.panahifar.ir/ahp/sshctl/issues +.SH AUTHOR +Amir Husayn Panahifar +.SH SEE ALSO +.BR ssh (1), +.BR ssh_config (5), +.BR sshd (8)