docs: add readme, changelog, contributing and man page

This commit is contained in:
ahp
2026-09-12 13:47:32 +03:30
parent e3a01c4d3a
commit 5799a791c2
4 changed files with 687 additions and 0 deletions
+62
View File
@@ -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)
+66
View File
@@ -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
```
+359
View File
@@ -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 <path>` | 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)
+200
View File
@@ -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)