Implements Phase 2 of the automated deployment pipeline. This commit adds a Python CLI tool that automates the creation of NixOS host configurations, eliminating manual boilerplate and reducing errors. Features: - Python CLI using typer framework with rich terminal UI - Comprehensive validation (hostname format/uniqueness, IP subnet/uniqueness) - Jinja2 templates for NixOS configurations - Automatic updates to flake.nix and terraform/vms.tf - Support for both static IP and DHCP configurations - Dry-run mode for safe previews - Packaged as Nix derivation and added to devShell Usage: create-host --hostname myhost --ip 10.69.13.50/24 The tool generates: - hosts/<hostname>/default.nix - hosts/<hostname>/configuration.nix - Updates flake.nix with new nixosConfigurations entry - Updates terraform/vms.tf with new VM definition All generated configurations include full system imports (monitoring, SOPS, autoupgrade, etc.) and are validated with nix flake check and tofu validate. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
187 lines
6.0 KiB
Python
187 lines
6.0 KiB
Python
"""CLI tool for generating NixOS host configurations."""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import typer
|
|
from rich.console import Console
|
|
from rich.panel import Panel
|
|
from rich.table import Table
|
|
|
|
from generators import generate_host_files
|
|
from manipulators import update_flake_nix, update_terraform_vms
|
|
from models import HostConfig
|
|
from validators import (
|
|
validate_hostname_format,
|
|
validate_hostname_unique,
|
|
validate_ip_subnet,
|
|
validate_ip_unique,
|
|
)
|
|
|
|
app = typer.Typer(
|
|
name="create-host",
|
|
help="Generate NixOS host configurations for homelab infrastructure",
|
|
add_completion=False,
|
|
)
|
|
console = Console()
|
|
|
|
|
|
def get_repo_root() -> Path:
|
|
"""Get the repository root directory."""
|
|
# Use current working directory as repo root
|
|
# The tool should be run from the repository root
|
|
return Path.cwd()
|
|
|
|
|
|
@app.callback(invoke_without_command=True)
|
|
def main(
|
|
ctx: typer.Context,
|
|
hostname: Optional[str] = typer.Option(None, "--hostname", help="Hostname for the new host"),
|
|
ip: Optional[str] = typer.Option(
|
|
None, "--ip", help="Static IP address with CIDR (e.g., 10.69.13.50/24). Omit for DHCP."
|
|
),
|
|
cpu: int = typer.Option(2, "--cpu", help="Number of CPU cores"),
|
|
memory: int = typer.Option(2048, "--memory", help="Memory in MB"),
|
|
disk: str = typer.Option("20G", "--disk", help="Disk size (e.g., 20G, 50G, 100G)"),
|
|
dry_run: bool = typer.Option(False, "--dry-run", help="Preview changes without creating files"),
|
|
) -> None:
|
|
"""
|
|
Create a new NixOS host configuration.
|
|
|
|
Generates host configuration files, updates flake.nix, and adds Terraform VM definition.
|
|
"""
|
|
# Show help if no hostname provided
|
|
if hostname is None:
|
|
console.print("[bold red]Error:[/bold red] --hostname is required\n")
|
|
ctx.get_help()
|
|
sys.exit(1)
|
|
|
|
try:
|
|
# Build configuration
|
|
config = HostConfig(
|
|
hostname=hostname,
|
|
ip=ip,
|
|
cpu=cpu,
|
|
memory=memory,
|
|
disk=disk,
|
|
)
|
|
|
|
# Get repository root
|
|
repo_root = get_repo_root()
|
|
|
|
# Validate configuration
|
|
console.print("\n[bold blue]Validating configuration...[/bold blue]")
|
|
|
|
config.validate()
|
|
validate_hostname_format(hostname)
|
|
validate_hostname_unique(hostname, repo_root)
|
|
|
|
if ip:
|
|
validate_ip_subnet(ip)
|
|
validate_ip_unique(ip, repo_root)
|
|
|
|
console.print("[green]✓[/green] All validations passed\n")
|
|
|
|
# Display configuration summary
|
|
display_config_summary(config)
|
|
|
|
# Dry run mode - exit before making changes
|
|
if dry_run:
|
|
console.print("\n[yellow]DRY RUN MODE - No files will be created[/yellow]\n")
|
|
display_dry_run_summary(config, repo_root)
|
|
return
|
|
|
|
# Generate files
|
|
console.print("\n[bold blue]Generating host configuration...[/bold blue]")
|
|
|
|
generate_host_files(config, repo_root)
|
|
console.print(f"[green]✓[/green] Created hosts/{hostname}/default.nix")
|
|
console.print(f"[green]✓[/green] Created hosts/{hostname}/configuration.nix")
|
|
|
|
update_flake_nix(config, repo_root)
|
|
console.print("[green]✓[/green] Updated flake.nix")
|
|
|
|
update_terraform_vms(config, repo_root)
|
|
console.print("[green]✓[/green] Updated terraform/vms.tf")
|
|
|
|
# Success message
|
|
console.print("\n[bold green]✓ Host configuration generated successfully![/bold green]\n")
|
|
|
|
# Display next steps
|
|
display_next_steps(hostname)
|
|
|
|
except ValueError as e:
|
|
console.print(f"\n[bold red]Error:[/bold red] {e}\n", style="red")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
console.print(f"\n[bold red]Unexpected error:[/bold red] {e}\n", style="red")
|
|
sys.exit(1)
|
|
|
|
|
|
def display_config_summary(config: HostConfig) -> None:
|
|
"""Display configuration summary table."""
|
|
table = Table(title="Host Configuration", show_header=False)
|
|
table.add_column("Property", style="cyan")
|
|
table.add_column("Value", style="white")
|
|
|
|
table.add_row("Hostname", config.hostname)
|
|
table.add_row("Domain", config.domain)
|
|
table.add_row("Network Mode", "Static IP" if config.is_static_ip else "DHCP")
|
|
|
|
if config.is_static_ip:
|
|
table.add_row("IP Address", config.ip)
|
|
table.add_row("Gateway", config.gateway)
|
|
|
|
table.add_row("DNS Servers", ", ".join(config.nameservers))
|
|
table.add_row("CPU Cores", str(config.cpu))
|
|
table.add_row("Memory", f"{config.memory} MB")
|
|
table.add_row("Disk Size", config.disk)
|
|
table.add_row("State Version", config.state_version)
|
|
|
|
console.print(table)
|
|
|
|
|
|
def display_dry_run_summary(config: HostConfig, repo_root: Path) -> None:
|
|
"""Display what would be created in dry run mode."""
|
|
console.print("[bold]Files that would be created:[/bold]")
|
|
console.print(f" • {repo_root}/hosts/{config.hostname}/default.nix")
|
|
console.print(f" • {repo_root}/hosts/{config.hostname}/configuration.nix")
|
|
|
|
console.print("\n[bold]Files that would be modified:[/bold]")
|
|
console.print(f" • {repo_root}/flake.nix (add nixosConfigurations.{config.hostname})")
|
|
console.print(f" • {repo_root}/terraform/vms.tf (add VM definition)")
|
|
|
|
|
|
def display_next_steps(hostname: str) -> None:
|
|
"""Display next steps after successful generation."""
|
|
next_steps = f"""[bold cyan]Next Steps:[/bold cyan]
|
|
|
|
1. Review changes:
|
|
[white]git diff[/white]
|
|
|
|
2. Verify NixOS configuration:
|
|
[white]nix flake check
|
|
nix build .#nixosConfigurations.{hostname}.config.system.build.toplevel[/white]
|
|
|
|
3. Verify Terraform configuration:
|
|
[white]cd terraform
|
|
tofu validate
|
|
tofu plan[/white]
|
|
|
|
4. Commit changes:
|
|
[white]git add hosts/{hostname} flake.nix terraform/vms.tf
|
|
git commit -m "hosts: add {hostname} configuration"[/white]
|
|
|
|
5. Deploy VM (after merging to master):
|
|
[white]cd terraform
|
|
tofu apply[/white]
|
|
|
|
6. Bootstrap the host (see Phase 3 of deployment pipeline)
|
|
"""
|
|
console.print(Panel(next_steps, border_style="cyan"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app()
|