mirror of
https://github.com/basnijholt/compose-farm.git
synced 2026-03-06 10:52:03 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aabdd550ba | ||
|
|
8ff60a1e3e | ||
|
|
2497bd727a |
@@ -43,6 +43,10 @@ Icons use [Lucide](https://lucide.dev/). Add new icons as macros in `web/templat
|
||||
7. **State tracking**: Tracks where services are deployed for auto-migration
|
||||
8. **Pre-flight checks**: Verifies NFS mounts and Docker networks exist before starting/migrating
|
||||
|
||||
## Code Style
|
||||
|
||||
- **Imports at top level**: Never add imports inside functions unless they are explicitly marked with `# noqa: PLC0415` and a comment explaining it speeds up CLI startup. Heavy modules like `pydantic`, `yaml`, and `rich.table` are lazily imported to keep `cf --help` fast.
|
||||
|
||||
## Communication Notes
|
||||
|
||||
- Clarify ambiguous wording (e.g., homophones like "right"/"write", "their"/"there").
|
||||
|
||||
@@ -149,8 +149,14 @@ def _check_ssh_connectivity(cfg: Config) -> list[str]:
|
||||
|
||||
async def check_host(host_name: str) -> tuple[str, bool]:
|
||||
host = cfg.hosts[host_name]
|
||||
result = await run_command(host, "echo ok", host_name, stream=False)
|
||||
return host_name, result.success
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
run_command(host, "echo ok", host_name, stream=False),
|
||||
timeout=5.0,
|
||||
)
|
||||
return host_name, result.success
|
||||
except TimeoutError:
|
||||
return host_name, False
|
||||
|
||||
results = run_parallel_with_progress(
|
||||
"Checking SSH connectivity",
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import subprocess
|
||||
from typing import Annotated
|
||||
from typing import TYPE_CHECKING, Annotated
|
||||
|
||||
import typer
|
||||
|
||||
from compose_farm.cli.app import app
|
||||
from compose_farm.cli.common import ConfigOption, load_config_or_exit
|
||||
from compose_farm.cli.common import ConfigOption, load_config_or_exit, run_parallel_with_progress
|
||||
from compose_farm.console import console, err_console
|
||||
from compose_farm.executor import run_command
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from compose_farm.config import Host
|
||||
|
||||
from compose_farm.ssh_keys import (
|
||||
SSH_KEY_PATH,
|
||||
SSH_PUBKEY_PATH,
|
||||
@@ -192,7 +198,7 @@ def ssh_setup(
|
||||
|
||||
|
||||
@ssh_app.command("status")
|
||||
def ssh_status( # noqa: PLR0912 - branches are clear and readable
|
||||
def ssh_status(
|
||||
config: ConfigOption = None,
|
||||
) -> None:
|
||||
"""Show SSH key status and host connectivity."""
|
||||
@@ -232,50 +238,42 @@ def ssh_status( # noqa: PLR0912 - branches are clear and readable
|
||||
console.print(" [dim]No remote hosts configured[/]")
|
||||
return
|
||||
|
||||
async def check_host(item: tuple[str, Host]) -> tuple[str, str, str]:
|
||||
"""Check connectivity to a single host."""
|
||||
host_name, host = item
|
||||
target = f"{host.user}@{host.address}"
|
||||
if host.port != _DEFAULT_SSH_PORT:
|
||||
target += f":{host.port}"
|
||||
|
||||
try:
|
||||
result = await asyncio.wait_for(
|
||||
run_command(host, "echo ok", host_name, stream=False),
|
||||
timeout=5.0,
|
||||
)
|
||||
status = "[green]OK[/]" if result.success else "[red]Auth failed[/]"
|
||||
except TimeoutError:
|
||||
status = "[red]Timeout (5s)[/]"
|
||||
except Exception as e:
|
||||
status = f"[red]Error: {e}[/]"
|
||||
|
||||
return host_name, target, status
|
||||
|
||||
# Check connectivity in parallel with progress bar
|
||||
results = run_parallel_with_progress(
|
||||
"Checking hosts",
|
||||
list(remote_hosts.items()),
|
||||
check_host,
|
||||
)
|
||||
|
||||
# Build table from results
|
||||
table = Table(show_header=True, header_style="bold")
|
||||
table.add_column("Host")
|
||||
table.add_column("Address")
|
||||
table.add_column("Status")
|
||||
|
||||
for host_name, host in remote_hosts.items():
|
||||
target = f"{host.user}@{host.address}"
|
||||
if host.port != _DEFAULT_SSH_PORT:
|
||||
target += f":{host.port}"
|
||||
|
||||
# Test connectivity with a simple command
|
||||
cmd = [
|
||||
"ssh",
|
||||
"-o",
|
||||
"StrictHostKeyChecking=no",
|
||||
"-o",
|
||||
"UserKnownHostsFile=/dev/null",
|
||||
"-o",
|
||||
"BatchMode=yes", # Fail immediately if password required
|
||||
"-o",
|
||||
"ConnectTimeout=5",
|
||||
]
|
||||
|
||||
# Add key file if it exists
|
||||
if key_exists():
|
||||
cmd.extend(["-i", str(SSH_KEY_PATH)])
|
||||
|
||||
if host.port != _DEFAULT_SSH_PORT:
|
||||
cmd.extend(["-p", str(host.port)])
|
||||
|
||||
cmd.extend([f"{host.user}@{host.address}", "echo ok"])
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd, check=False, capture_output=True, timeout=10, env=get_ssh_env()
|
||||
)
|
||||
if result.returncode == 0:
|
||||
table.add_row(host_name, target, "[green]OK[/]")
|
||||
else:
|
||||
table.add_row(host_name, target, "[red]Auth failed[/]")
|
||||
except subprocess.TimeoutExpired:
|
||||
table.add_row(host_name, target, "[red]Timeout[/]")
|
||||
except Exception as e:
|
||||
table.add_row(host_name, target, f"[red]Error: {e}[/]")
|
||||
# Sort by host name for consistent order
|
||||
for host_name, target, status in sorted(results, key=lambda r: r[0]):
|
||||
table.add_row(host_name, target, status)
|
||||
|
||||
console.print(table)
|
||||
|
||||
|
||||
@@ -411,6 +411,16 @@ function initPage() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initPage();
|
||||
initKeyboardShortcuts();
|
||||
|
||||
// Handle ?action= parameter (from command palette navigation)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const action = params.get('action');
|
||||
if (action && window.location.pathname === '/') {
|
||||
// Clear the URL parameter
|
||||
history.replaceState({}, '', '/');
|
||||
// Trigger the action
|
||||
htmx.ajax('POST', `/api/${action}`, {swap: 'none'});
|
||||
}
|
||||
});
|
||||
|
||||
// Re-initialize after HTMX swaps main content
|
||||
@@ -498,12 +508,20 @@ document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||
|
||||
const post = (url) => () => htmx.ajax('POST', url, {swap: 'none'});
|
||||
const nav = (url) => () => window.location.href = url;
|
||||
// Navigate to dashboard and trigger action (or just POST if already on dashboard)
|
||||
const dashboardAction = (endpoint) => () => {
|
||||
if (window.location.pathname === '/') {
|
||||
htmx.ajax('POST', `/api/${endpoint}`, {swap: 'none'});
|
||||
} else {
|
||||
window.location.href = `/?action=${endpoint}`;
|
||||
}
|
||||
};
|
||||
const cmd = (type, name, desc, action, icon = null) => ({ type, name, desc, action, icon });
|
||||
|
||||
function buildCommands() {
|
||||
const actions = [
|
||||
cmd('action', 'Apply', 'Make reality match config', post('/api/apply'), icons.check),
|
||||
cmd('action', 'Refresh', 'Update state from reality', post('/api/refresh'), icons.refresh_cw),
|
||||
cmd('action', 'Apply', 'Make reality match config', dashboardAction('apply'), icons.check),
|
||||
cmd('action', 'Refresh', 'Update state from reality', dashboardAction('refresh'), icons.refresh_cw),
|
||||
cmd('nav', 'Dashboard', 'Go to dashboard', nav('/'), icons.home),
|
||||
cmd('nav', 'Console', 'Go to console', nav('/console'), icons.terminal),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user