"""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"), force: bool = typer.Option(False, "--force", help="Overwrite existing host configuration"), ) -> 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) # Skip uniqueness checks in force mode if not force: validate_hostname_unique(hostname, repo_root) if ip: validate_ip_unique(ip, repo_root) else: # Check if we're actually overwriting something host_dir = repo_root / "hosts" / hostname if host_dir.exists(): console.print(f"[yellow]⚠[/yellow] Updating existing host configuration for {hostname}") if ip: validate_ip_subnet(ip) 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) action = "Updated" if force else "Created" console.print(f"[green]✓[/green] {action} hosts/{hostname}/default.nix") console.print(f"[green]✓[/green] {action} hosts/{hostname}/configuration.nix") update_flake_nix(config, repo_root, force=force) console.print("[green]✓[/green] Updated flake.nix") update_terraform_vms(config, repo_root, force=force) 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()