297 lines
9.5 KiB
Python
297 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate Ansible playbooks from existing docker-compose files in the homelab repo.
|
|
This script scans the hosts/ directory and creates deployment playbooks.
|
|
"""
|
|
|
|
import os
|
|
import yaml
|
|
from pathlib import Path
|
|
from collections import defaultdict
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent.parent
|
|
HOSTS_DIR = REPO_ROOT / "hosts"
|
|
ANSIBLE_DIR = Path(__file__).parent
|
|
PLAYBOOKS_DIR = ANSIBLE_DIR / "playbooks"
|
|
HOST_VARS_DIR = ANSIBLE_DIR / "host_vars"
|
|
|
|
# Mapping of directory names to ansible host names
|
|
HOST_MAPPING = {
|
|
"atlantis": "atlantis",
|
|
"calypso": "calypso",
|
|
"setillo": "setillo",
|
|
"guava": "guava",
|
|
"concord-nuc": "concord_nuc",
|
|
"anubis": "anubis",
|
|
"homelab-vm": "homelab_vm",
|
|
"chicago-vm": "chicago_vm",
|
|
"bulgaria-vm": "bulgaria_vm",
|
|
"contabo-vm": "contabo_vm",
|
|
"rpi5-vish": "rpi5_vish",
|
|
"tdarr-node": "tdarr_node",
|
|
}
|
|
|
|
# Host categories for grouping
|
|
HOST_CATEGORIES = {
|
|
"synology": ["atlantis", "calypso", "setillo"],
|
|
"physical": ["guava", "concord-nuc", "anubis"],
|
|
"vms": ["homelab-vm", "chicago-vm", "bulgaria-vm", "contabo-vm", "matrix-ubuntu-vm"],
|
|
"edge": ["rpi5-vish", "nvidia_shield"],
|
|
"proxmox": ["tdarr-node"],
|
|
}
|
|
|
|
|
|
def find_compose_files():
|
|
"""Find all docker-compose files in the hosts directory."""
|
|
compose_files = defaultdict(list)
|
|
|
|
for yaml_file in HOSTS_DIR.rglob("*.yaml"):
|
|
if ".git" in str(yaml_file):
|
|
continue
|
|
compose_files[yaml_file.parent].append(yaml_file)
|
|
|
|
for yml_file in HOSTS_DIR.rglob("*.yml"):
|
|
if ".git" in str(yml_file):
|
|
continue
|
|
compose_files[yml_file.parent].append(yml_file)
|
|
|
|
return compose_files
|
|
|
|
|
|
def get_host_from_path(file_path):
|
|
"""Extract REDACTED_APP_PASSWORD path."""
|
|
parts = file_path.relative_to(HOSTS_DIR).parts
|
|
|
|
# Structure: hosts/<category>/<host>/...
|
|
if len(parts) >= 2:
|
|
category = parts[0]
|
|
host = parts[1]
|
|
return category, host
|
|
return None, None
|
|
|
|
|
|
def extract_service_name(file_path):
|
|
"""Extract service name from file path."""
|
|
# Get the service name from parent directory or filename
|
|
if file_path.name in ["docker-compose.yml", "docker-compose.yaml"]:
|
|
return file_path.parent.name
|
|
else:
|
|
return file_path.stem.replace("-", "_").replace(".", "_")
|
|
|
|
|
|
def is_compose_file(file_path):
|
|
"""Check if file looks like a docker-compose file."""
|
|
try:
|
|
with open(file_path, 'r') as f:
|
|
content = yaml.safe_load(f)
|
|
if content and isinstance(content, dict):
|
|
return 'services' in content or 'version' in content
|
|
except:
|
|
pass
|
|
return False
|
|
|
|
|
|
def generate_service_vars(host, services):
|
|
"""Generate host_vars with service definitions."""
|
|
service_list = []
|
|
|
|
for service_path, service_name in services:
|
|
rel_path = service_path.relative_to(REPO_ROOT)
|
|
|
|
# Determine the stack directory name
|
|
if service_path.name in ["docker-compose.yml", "docker-compose.yaml"]:
|
|
stack_dir = service_path.parent.name
|
|
else:
|
|
stack_dir = service_name
|
|
|
|
service_entry = {
|
|
"name": service_name,
|
|
"stack_dir": stack_dir,
|
|
"compose_file": str(rel_path),
|
|
"enabled": True,
|
|
}
|
|
|
|
# Check for .env file
|
|
env_file = service_path.parent / ".env"
|
|
stack_env = service_path.parent / "stack.env"
|
|
if env_file.exists():
|
|
service_entry["env_file"] = str(env_file.relative_to(REPO_ROOT))
|
|
elif stack_env.exists():
|
|
service_entry["env_file"] = str(stack_env.relative_to(REPO_ROOT))
|
|
|
|
service_list.append(service_entry)
|
|
|
|
return service_list
|
|
|
|
|
|
def generate_host_playbook(host_name, ansible_host, services, category):
|
|
"""Generate a playbook for a specific host."""
|
|
|
|
# Create header comment
|
|
header = f"""---
|
|
# Deployment playbook for {host_name}
|
|
# Category: {category}
|
|
# Services: {len(services)}
|
|
#
|
|
# Usage:
|
|
# ansible-playbook playbooks/deploy_{ansible_host}.yml
|
|
# ansible-playbook playbooks/deploy_{ansible_host}.yml -e "stack_deploy=false"
|
|
# ansible-playbook playbooks/deploy_{ansible_host}.yml --check
|
|
|
|
"""
|
|
|
|
playbook = [
|
|
{
|
|
"name": f"Deploy services to {host_name}",
|
|
"hosts": ansible_host,
|
|
"gather_facts": True,
|
|
"vars": {
|
|
"services": "{{ host_services | default([]) }}"
|
|
},
|
|
"tasks": [
|
|
{
|
|
"name": "Display deployment info",
|
|
"ansible.builtin.debug": {
|
|
"msg": "Deploying {{ services | length }} services to {{ inventory_hostname }}"
|
|
}
|
|
},
|
|
{
|
|
"name": "Ensure docker data directory exists",
|
|
"ansible.builtin.file": {
|
|
"path": "{{ docker_data_path }}",
|
|
"state": "directory",
|
|
"mode": "0755"
|
|
}
|
|
},
|
|
{
|
|
"name": "Deploy each enabled service",
|
|
"ansible.builtin.include_role": {
|
|
"name": "docker_stack"
|
|
},
|
|
"vars": {
|
|
"stack_name": "{{ item.stack_dir }}",
|
|
"stack_compose_file": "{{ item.compose_file }}",
|
|
"stack_env_file": "{{ item.env_file | default(omit) }}"
|
|
},
|
|
"loop": "{{ services }}",
|
|
"loop_control": {
|
|
"label": "{{ item.name }}"
|
|
},
|
|
"when": "item.enabled | default(true)"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
return header, playbook
|
|
|
|
|
|
def main():
|
|
"""Main function to generate all playbooks."""
|
|
print("=" * 60)
|
|
print("Generating Ansible Playbooks from Homelab Repository")
|
|
print("=" * 60)
|
|
|
|
# Ensure directories exist
|
|
PLAYBOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
HOST_VARS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Find all compose files
|
|
compose_files = find_compose_files()
|
|
|
|
# Organize by host
|
|
hosts_services = defaultdict(list)
|
|
|
|
for directory, files in compose_files.items():
|
|
category, host = get_host_from_path(directory)
|
|
if not host:
|
|
continue
|
|
|
|
for f in files:
|
|
if is_compose_file(f):
|
|
service_name = extract_service_name(f)
|
|
hosts_services[(category, host)].append((f, service_name))
|
|
|
|
# Generate playbooks and host_vars
|
|
all_hosts = {}
|
|
|
|
for (category, host), services in sorted(hosts_services.items()):
|
|
ansible_host = HOST_MAPPING.get(host, host.replace("-", "_"))
|
|
|
|
print(f"\n[{category}/{host}] Found {len(services)} services:")
|
|
for service_path, service_name in services:
|
|
print(f" - {service_name}")
|
|
|
|
# Generate host_vars
|
|
service_vars = generate_service_vars(host, services)
|
|
host_vars = {
|
|
"host_services": service_vars
|
|
}
|
|
|
|
host_vars_file = HOST_VARS_DIR / f"{ansible_host}.yml"
|
|
with open(host_vars_file, 'w') as f:
|
|
f.write("---\n")
|
|
f.write(f"# Auto-generated host variables for {host}\n")
|
|
f.write(f"# Services deployed to this host\n\n")
|
|
yaml.dump(host_vars, f, default_flow_style=False, sort_keys=False)
|
|
|
|
# Generate individual host playbook
|
|
header, playbook = generate_host_playbook(host, ansible_host, services, category)
|
|
playbook_file = PLAYBOOKS_DIR / f"deploy_{ansible_host}.yml"
|
|
with open(playbook_file, 'w') as f:
|
|
f.write(header)
|
|
yaml.dump(playbook, f, default_flow_style=False, sort_keys=False)
|
|
|
|
all_hosts[ansible_host] = {
|
|
"category": category,
|
|
"host": host,
|
|
"services": len(services)
|
|
}
|
|
|
|
# Generate master playbook
|
|
master_playbook = [
|
|
{
|
|
"name": "Deploy all homelab services",
|
|
"hosts": "localhost",
|
|
"gather_facts": False,
|
|
"tasks": [
|
|
{
|
|
"name": "Display deployment plan",
|
|
"ansible.builtin.debug": {
|
|
"msg": "Deploying services to all hosts. Use --limit to target specific hosts."
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
|
|
# Add imports for each host
|
|
for ansible_host, info in sorted(all_hosts.items()):
|
|
master_playbook.append({
|
|
"name": f"Deploy to {info['host']} ({info['services']} services)",
|
|
"ansible.builtin.import_playbook": f"playbooks/deploy_{ansible_host}.yml",
|
|
"tags": [info['category'], ansible_host]
|
|
})
|
|
|
|
master_file = ANSIBLE_DIR / "site.yml"
|
|
with open(master_file, 'w') as f:
|
|
f.write("---\n")
|
|
f.write("# Master Homelab Deployment Playbook\n")
|
|
f.write("# Auto-generated from docker-compose files\n")
|
|
f.write("#\n")
|
|
f.write("# Usage:\n")
|
|
f.write("# Deploy everything: ansible-playbook site.yml\n")
|
|
f.write("# Deploy specific host: ansible-playbook site.yml --limit atlantis\n")
|
|
f.write("# Deploy by category: ansible-playbook site.yml --tags synology\n")
|
|
f.write("#\n\n")
|
|
yaml.dump(master_playbook, f, default_flow_style=False, sort_keys=False)
|
|
|
|
print(f"\n{'=' * 60}")
|
|
print(f"Generated playbooks for {len(all_hosts)} hosts")
|
|
print(f"Master playbook: {master_file}")
|
|
print("=" * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|