200 lines
6.2 KiB
Python
200 lines
6.2 KiB
Python
"""File generation using Jinja2 templates."""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from jinja2 import Environment, BaseLoader, TemplateNotFound
|
|
|
|
from models import HostConfig
|
|
|
|
|
|
class PackageTemplateLoader(BaseLoader):
|
|
"""Custom Jinja2 loader that works with both dev and installed packages."""
|
|
|
|
def __init__(self):
|
|
# Try to find templates in multiple locations
|
|
self.template_dirs = []
|
|
|
|
# Location 1: Development (scripts/create-host/templates)
|
|
dev_dir = Path(__file__).parent / "templates"
|
|
if dev_dir.exists():
|
|
self.template_dirs.append(dev_dir)
|
|
|
|
# Location 2: Installed via Nix (../share/create-host/templates from bin dir)
|
|
# When installed via Nix, __file__ is in lib/python3.X/site-packages/
|
|
# and templates are in ../../../share/create-host/templates
|
|
for site_path in sys.path:
|
|
site_dir = Path(site_path)
|
|
# Try to find the Nix store path
|
|
if "site-packages" in str(site_dir):
|
|
# Go up to the package root (e.g., /nix/store/xxx-create-host-0.1.0)
|
|
pkg_root = site_dir.parent.parent.parent
|
|
share_templates = pkg_root / "share" / "create-host" / "templates"
|
|
if share_templates.exists():
|
|
self.template_dirs.append(share_templates)
|
|
|
|
# Location 3: Fallback - sys.path templates
|
|
for site_path in sys.path:
|
|
site_templates = Path(site_path) / "templates"
|
|
if site_templates.exists():
|
|
self.template_dirs.append(site_templates)
|
|
|
|
def get_source(self, environment, template):
|
|
for template_dir in self.template_dirs:
|
|
template_path = template_dir / template
|
|
if template_path.exists():
|
|
mtime = template_path.stat().st_mtime
|
|
source = template_path.read_text()
|
|
return source, str(template_path), lambda: mtime == template_path.stat().st_mtime
|
|
|
|
raise TemplateNotFound(template)
|
|
|
|
|
|
def generate_host_files(config: HostConfig, repo_root: Path) -> None:
|
|
"""
|
|
Generate host configuration files from templates.
|
|
|
|
Args:
|
|
config: Host configuration
|
|
repo_root: Path to repository root
|
|
"""
|
|
# Setup Jinja2 environment with custom loader
|
|
env = Environment(
|
|
loader=PackageTemplateLoader(),
|
|
trim_blocks=True,
|
|
lstrip_blocks=True,
|
|
)
|
|
|
|
# Create host directory
|
|
host_dir = repo_root / "hosts" / config.hostname
|
|
host_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Generate default.nix
|
|
default_template = env.get_template("default.nix.j2")
|
|
default_content = default_template.render(hostname=config.hostname)
|
|
(host_dir / "default.nix").write_text(default_content)
|
|
|
|
# Generate configuration.nix
|
|
config_template = env.get_template("configuration.nix.j2")
|
|
config_content = config_template.render(
|
|
hostname=config.hostname,
|
|
domain=config.domain,
|
|
nameservers=config.nameservers,
|
|
is_static_ip=config.is_static_ip,
|
|
ip=config.ip,
|
|
gateway=config.gateway,
|
|
state_version=config.state_version,
|
|
)
|
|
(host_dir / "configuration.nix").write_text(config_content)
|
|
|
|
|
|
def generate_vault_terraform(hostname: str, repo_root: Path) -> None:
|
|
"""
|
|
Generate or update Vault Terraform configuration for a new host.
|
|
|
|
Creates/updates terraform/vault/hosts-generated.tf with:
|
|
- Host policy granting access to hosts/<hostname>/* secrets
|
|
- AppRole configuration for the host
|
|
- Placeholder secret entry (user adds actual secrets separately)
|
|
|
|
Args:
|
|
hostname: Hostname for the new host
|
|
repo_root: Path to repository root
|
|
"""
|
|
vault_tf_path = repo_root / "terraform" / "vault" / "hosts-generated.tf"
|
|
|
|
# Read existing file if it exists, otherwise start with empty structure
|
|
if vault_tf_path.exists():
|
|
content = vault_tf_path.read_text()
|
|
else:
|
|
# Create initial file structure
|
|
content = """# WARNING: Auto-generated by create-host tool
|
|
# Manual edits will be overwritten when create-host is run
|
|
|
|
# Generated host policies
|
|
# Each host gets access to its own secrets under hosts/<hostname>/*
|
|
locals {
|
|
generated_host_policies = {
|
|
}
|
|
|
|
# Placeholder secrets - user should add actual secrets manually or via tofu
|
|
generated_secrets = {
|
|
}
|
|
}
|
|
|
|
# Create policies for generated hosts
|
|
resource "vault_policy" "generated_host_policies" {
|
|
for_each = local.generated_host_policies
|
|
|
|
name = "host-\${each.key}"
|
|
|
|
policy = <<-EOT
|
|
# Allow host to read its own secrets
|
|
%{for path in each.value.paths~}
|
|
path "${path}" {
|
|
capabilities = ["read", "list"]
|
|
}
|
|
%{endfor~}
|
|
EOT
|
|
}
|
|
|
|
# Create AppRoles for generated hosts
|
|
resource "vault_approle_auth_backend_role" "generated_hosts" {
|
|
for_each = local.generated_host_policies
|
|
|
|
backend = vault_auth_backend.approle.path
|
|
role_name = each.key
|
|
token_policies = ["host-\${each.key}"]
|
|
secret_id_ttl = 0 # Never expire (wrapped tokens provide time limit)
|
|
token_ttl = 3600
|
|
token_max_ttl = 3600
|
|
secret_id_num_uses = 0 # Unlimited uses
|
|
}
|
|
"""
|
|
|
|
# Parse existing policies from the file
|
|
import re
|
|
|
|
policies_match = re.search(
|
|
r'generated_host_policies = \{(.*?)\n \}',
|
|
content,
|
|
re.DOTALL
|
|
)
|
|
|
|
if policies_match:
|
|
policies_content = policies_match.group(1)
|
|
else:
|
|
policies_content = ""
|
|
|
|
# Check if hostname already exists
|
|
if f'"{hostname}"' in policies_content:
|
|
# Already exists, don't duplicate
|
|
return
|
|
|
|
# Add new policy entry
|
|
new_policy = f'''
|
|
"{hostname}" = {{
|
|
paths = [
|
|
"secret/data/hosts/{hostname}/*",
|
|
]
|
|
}}'''
|
|
|
|
# Insert before the closing brace
|
|
if policies_content.strip():
|
|
# There are existing entries, add after them
|
|
new_policies_content = policies_content.rstrip() + new_policy + "\n "
|
|
else:
|
|
# First entry
|
|
new_policies_content = new_policy + "\n "
|
|
|
|
# Replace the policies map
|
|
new_content = re.sub(
|
|
r'(generated_host_policies = \{)(.*?)(\n \})',
|
|
rf'\1{new_policies_content}\3',
|
|
content,
|
|
flags=re.DOTALL
|
|
)
|
|
|
|
# Write the updated file
|
|
vault_tf_path.write_text(new_content)
|