360 lines
11 KiB
Markdown
360 lines
11 KiB
Markdown
# sshctl
|
|
|
|
SSH configuration management toolkit — parse, validate, generate, and manage SSH configs at scale.
|
|
|
|
[]()
|
|
[](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)
|