#!/usr/bin/env python3 """ Generate comprehensive documentation for all homelab services. This script analyzes Docker Compose files and creates detailed documentation for each service. """ import os import yaml import re from pathlib import Path from typing import Dict, List, Any, Optional class ServiceDocumentationGenerator: def __init__(self, repo_path: str): self.repo_path = Path(repo_path) self.docs_path = self.repo_path / "docs" / "services" / "individual" self.docs_path.mkdir(parents=True, exist_ok=True) # Service categories for better organization self.categories = { 'media': ['plex', 'jellyfin', 'emby', 'tautulli', 'overseerr', 'ombi', 'radarr', 'sonarr', 'lidarr', 'readarr', 'bazarr', 'prowlarr', 'jackett', 'nzbget', 'sabnzbd', 'transmission', 'qbittorrent', 'deluge', 'immich', 'photoprism', 'navidrome', 'airsonic', 'calibre', 'komga', 'kavita'], 'monitoring': ['grafana', 'prometheus', 'uptime-kuma', 'uptimerobot', 'statping', 'healthchecks', 'netdata', 'zabbix', 'nagios', 'icinga', 'librenms', 'observium', 'cacti', 'ntopng', 'bandwidthd', 'darkstat', 'vnstat', 'smokeping', 'blackbox-exporter', 'node-exporter', 'cadvisor', 'exportarr'], 'productivity': ['nextcloud', 'owncloud', 'seafile', 'syncthing', 'filebrowser', 'paperless-ngx', 'paperless', 'docspell', 'teedy', 'bookstack', 'dokuwiki', 'tiddlywiki', 'outline', 'siyuan', 'logseq', 'obsidian', 'joplin', 'standardnotes', 'trilium', 'zettlr', 'typora', 'marktext', 'ghostwriter', 'remarkable', 'xournalpp', 'rnote', 'firefly-iii', 'actual-budget', 'budget-zen', 'maybe-finance', 'kresus', 'homebank', 'gnucash', 'ledger', 'beancount', 'plaintextaccounting'], 'development': ['gitea', 'gitlab', 'github', 'bitbucket', 'sourcehut', 'forgejo', 'cgit', 'gitweb', 'jenkins', 'drone', 'woodpecker', 'buildkite', 'teamcity', 'bamboo', 'travis', 'circleci', 'github-actions', 'gitlab-ci', 'azure-devops', 'aws-codebuild', 'portainer', 'yacht', 'dockge', 'lazydocker', 'ctop', 'dive', 'docker-compose-ui', 'docker-registry', 'harbor', 'quay', 'nexus', 'artifactory', 'verdaccio', 'npm-registry'], 'communication': ['matrix-synapse', 'element', 'riot', 'nheko', 'fluffychat', 'cinny', 'hydrogen', 'schildichat', 'mattermost', 'rocket-chat', 'zulip', 'slack', 'discord', 'telegram', 'signal', 'whatsapp', 'messenger', 'skype', 'zoom', 'jitsi', 'bigbluebutton', 'jami', 'briar', 'session', 'wickr', 'threema', 'wire', 'keybase', 'mastodon', 'pleroma', 'misskey', 'diaspora', 'friendica', 'hubzilla', 'peertube', 'pixelfed', 'lemmy', 'kbin'], 'security': ['vaultwarden', 'bitwarden', 'keepass', 'passbolt', 'psono', 'teampass', 'pleasant-password', 'authelia', 'authentik', 'keycloak', 'gluu', 'freeipa', 'openldap', 'active-directory', 'okta', 'auth0', 'firebase-auth', 'aws-cognito', 'azure-ad', 'google-identity', 'pihole', 'adguard', 'blocky', 'unbound', 'bind9', 'powerdns', 'coredns', 'technitium', 'wireguard', 'openvpn', 'ipsec', 'tinc', 'zerotier', 'tailscale', 'nebula', 'headscale'], 'networking': ['nginx', 'apache', 'caddy', 'traefik', 'haproxy', 'envoy', 'istio', 'linkerd', 'consul', 'vault', 'nomad', 'pfsense', 'opnsense', 'vyos', 'mikrotik', 'ubiquiti', 'tp-link', 'netgear', 'asus', 'linksys', 'dlink', 'zyxel', 'fortinet', 'sonicwall', 'watchguard', 'palo-alto', 'checkpoint', 'juniper', 'cisco', 'arista', 'cumulus', 'sonic', 'frr', 'quagga', 'bird', 'openbgpd'], 'storage': ['minio', 's3', 'ceph', 'glusterfs', 'moosefs', 'lizardfs', 'orangefs', 'lustre', 'beegfs', 'gpfs', 'hdfs', 'cassandra', 'mongodb', 'postgresql', 'mysql', 'mariadb', 'sqlite', 'redis', 'memcached', 'elasticsearch', 'solr', 'sphinx', 'whoosh', 'xapian', 'lucene', 'influxdb', 'prometheus', 'graphite', 'opentsdb', 'kairosdb', 'druid', 'clickhouse', 'timescaledb'], 'gaming': ['minecraft', 'factorio', 'satisfactory', 'valheim', 'terraria', 'starbound', 'dont-starve', 'project-zomboid', 'rust', 'ark', 'conan-exiles', 'space-engineers', 'astroneer', 'raft', 'green-hell', 'the-forest', 'subnautica', 'no-mans-sky', 'elite-dangerous', 'star-citizen', 'eve-online', 'world-of-warcraft', 'final-fantasy-xiv', 'guild-wars-2', 'elder-scrolls-online', 'destiny-2', 'warframe', 'path-of-exile', 'diablo', 'torchlight', 'grim-dawn', 'last-epoch'], 'ai': ['ollama', 'llamagpt', 'chatgpt', 'gpt4all', 'localai', 'text-generation-webui', 'koboldai', 'novelai', 'stable-diffusion', 'automatic1111', 'invokeai', 'comfyui', 'fooocus', 'easydiffusion', 'diffusionbee', 'draw-things', 'whisper', 'faster-whisper', 'vosk', 'deepspeech', 'wav2vec', 'espnet', 'kaldi', 'julius', 'pocketsphinx', 'festival', 'espeak', 'mary-tts', 'mimic', 'tacotron', 'wavenet', 'neural-voices'] } def find_compose_files(self) -> List[Path]: """Find all YAML files that contain Docker Compose configurations.""" compose_files = [] # Find all YAML files yaml_files = list(self.repo_path.rglob('*.yml')) + list(self.repo_path.rglob('*.yaml')) # Filter out files in docs, .git, and other non-service directories filtered_files = [] for file in yaml_files: path_parts = file.parts if any(part in path_parts for part in ['.git', 'docs', 'node_modules', '.vscode', '__pycache__', 'ansible']): continue # Check if file contains Docker Compose configuration try: with open(file, 'r', encoding='utf-8') as f: content = f.read() # Look for Docker Compose indicators if ('services:' in content and ('version:' in content or 'image:' in content or 'build:' in content)): filtered_files.append(file) except Exception as e: print(f"Warning: Could not read {file}: {e}") continue return sorted(filtered_files) def parse_compose_file(self, compose_file: Path) -> Dict[str, Any]: """Parse a docker-compose file and extract service information.""" try: with open(compose_file, 'r', encoding='utf-8') as f: content = yaml.safe_load(f) if not content or 'services' not in content: return {} # Extract metadata from file path relative_path = compose_file.relative_to(self.repo_path) host = relative_path.parts[0] if len(relative_path.parts) > 1 else 'unknown' services_info = {} for service_name, service_config in content['services'].items(): services_info[service_name] = { 'config': service_config, 'host': host, 'compose_file': str(relative_path), 'directory': str(compose_file.parent.relative_to(self.repo_path)) } return services_info except Exception as e: print(f"Error parsing {compose_file}: {e}") return {} def categorize_service(self, service_name: str, image: str = '') -> str: """Categorize a service based on its name and image.""" service_lower = service_name.lower().replace('-', '').replace('_', '') image_lower = image.lower() if image else '' for category, keywords in self.categories.items(): for keyword in keywords: keyword_clean = keyword.replace('-', '').replace('_', '') if keyword_clean in service_lower or keyword_clean in image_lower: return category return 'other' def extract_ports(self, service_config: Dict) -> List[str]: """Extract port mappings from service configuration.""" ports = [] if 'ports' in service_config: for port in service_config['ports']: if isinstance(port, str): ports.append(port) elif isinstance(port, dict): target = port.get('target', '') published = port.get('published', '') if target and published: ports.append(f"{published}:{target}") return ports def extract_volumes(self, service_config: Dict) -> List[str]: """Extract volume mappings from service configuration.""" volumes = [] if 'volumes' in service_config: for volume in service_config['volumes']: if isinstance(volume, str): volumes.append(volume) elif isinstance(volume, dict): source = volume.get('source', '') target = volume.get('target', '') if source and target: volumes.append(f"{source}:{target}") return volumes def extract_environment(self, service_config: Dict) -> Dict[str, str]: """Extract environment variables from service configuration.""" env_vars = {} if 'environment' in service_config: env = service_config['environment'] if isinstance(env, list): for var in env: if '=' in var: key, value = var.split('=', 1) env_vars[key] = value elif isinstance(env, dict): env_vars = env return env_vars def get_difficulty_level(self, service_name: str, service_config: Dict) -> str: """Determine difficulty level based on service complexity.""" # Advanced services (require significant expertise) advanced_keywords = [ 'gitlab', 'jenkins', 'kubernetes', 'consul', 'vault', 'nomad', 'elasticsearch', 'cassandra', 'mongodb-cluster', 'postgresql-cluster', 'matrix-synapse', 'mastodon', 'peertube', 'nextcloud', 'keycloak', 'authentik', 'authelia', 'traefik', 'istio', 'linkerd' ] # Intermediate services (require basic Docker/Linux knowledge) intermediate_keywords = [ 'grafana', 'prometheus', 'nginx', 'caddy', 'haproxy', 'wireguard', 'openvpn', 'pihole', 'adguard', 'vaultwarden', 'bitwarden', 'paperless', 'bookstack', 'dokuwiki', 'mattermost', 'rocket-chat', 'portainer', 'yacht', 'immich', 'photoprism', 'jellyfin', 'emby' ] service_lower = service_name.lower() image = service_config.get('image', '').lower() # Check for advanced complexity indicators has_depends_on = 'depends_on' in service_config has_multiple_volumes = len(service_config.get('volumes', [])) > 3 has_complex_networking = 'networks' in service_config and len(service_config['networks']) > 1 has_build_config = 'build' in service_config if any(keyword in service_lower or keyword in image for keyword in advanced_keywords): return 'šŸ”“' elif (any(keyword in service_lower or keyword in image for keyword in intermediate_keywords) or has_depends_on or has_multiple_volumes or has_complex_networking or has_build_config): return '🟔' else: return '🟢' def generate_service_documentation(self, service_name: str, service_info: Dict) -> str: """Generate comprehensive documentation for a single service.""" config = service_info['config'] host = service_info['host'] compose_file = service_info['compose_file'] directory = service_info['directory'] # Extract key information image = config.get('image', 'Unknown') ports = self.extract_ports(config) volumes = self.extract_volumes(config) env_vars = self.extract_environment(config) category = self.categorize_service(service_name, image) difficulty = self.get_difficulty_level(service_name, config) # Generate documentation content doc_content = f"""# {service_name.title().replace('-', ' ').replace('_', ' ')} **{difficulty} {category.title()} Service** ## šŸ“‹ Service Overview | Property | Value | |----------|-------| | **Service Name** | {service_name} | | **Host** | {host} | | **Category** | {category.title()} | | **Difficulty** | {difficulty} | | **Docker Image** | `{image}` | | **Compose File** | `{compose_file}` | | **Directory** | `{directory}` | ## šŸŽÆ Purpose {self.get_service_description(service_name, image, category)} ## šŸš€ Quick Start ### Prerequisites - Docker and Docker Compose installed - Basic understanding of REDACTED_APP_PASSWORD - Access to the host system ({host}) ### Deployment ```bash # Navigate to service directory cd {directory} # Start the service docker-compose up -d # Check service status docker-compose ps # View logs docker-compose logs -f {service_name} ``` ## šŸ”§ Configuration ### Docker Compose Configuration ```yaml {self.format_compose_config(config)} ``` ### Environment Variables {self.format_environment_variables(env_vars)} ### Port Mappings {self.format_ports(ports)} ### Volume Mappings {self.format_volumes(volumes)} ## 🌐 Access Information {self.generate_access_info(service_name, ports, host)} ## šŸ”’ Security Considerations {self.generate_security_info(service_name, config)} ## šŸ“Š Resource Requirements {self.generate_resource_info(config)} ## šŸ” Health Monitoring {self.generate_health_info(config)} ## 🚨 Troubleshooting ### Common Issues {self.generate_troubleshooting_info(service_name, category)} ### Useful Commands ```bash # Check service status docker-compose ps # View real-time logs docker-compose logs -f {service_name} # Restart service docker-compose restart {service_name} # Update service docker-compose pull {service_name} docker-compose up -d {service_name} # Access service shell docker-compose exec {service_name} /bin/bash # or docker-compose exec {service_name} /bin/sh ``` ## šŸ“š Additional Resources {self.generate_additional_resources(service_name, image)} ## šŸ”— Related Services {self.generate_related_services(service_name, category, host)} --- *This documentation is auto-generated from the Docker Compose configuration. For the most up-to-date information, refer to the official documentation and the actual compose file.* **Last Updated**: {self.get_current_date()} **Configuration Source**: `{compose_file}` """ return doc_content def get_service_description(self, service_name: str, image: str, category: str) -> str: """Generate a description for the service based on its name and category.""" descriptions = { 'plex': 'Plex Media Server organizes video, music and photos from personal media libraries and streams them to smart TVs, streaming boxes and mobile devices.', 'jellyfin': 'Jellyfin is a Free Software Media System that puts you in control of managing and streaming your media.', 'grafana': 'Grafana is the open source analytics & monitoring solution for every database.', 'prometheus': 'Prometheus is an open-source systems monitoring and alerting toolkit.', 'uptime-kuma': 'Uptime Kuma is a fancy self-hosted monitoring tool.', 'nginx': 'NGINX is a web server that can also be used as a reverse proxy, load balancer, mail proxy and HTTP cache.', 'traefik': 'Traefik is a modern HTTP reverse proxy and load balancer that makes deploying microservices easy.', 'portainer': 'Portainer is a lightweight management UI which allows you to easily manage your different Docker environments.', 'vaultwarden': 'Vaultwarden is an alternative implementation of the Bitwarden server API written in Rust and compatible with upstream Bitwarden clients.', 'pihole': 'Pi-hole is a DNS sinkhole that protects your devices from unwanted content, without installing any client-side software.', 'adguard': 'AdGuard Home is a network-wide software for blocking ads & tracking.', 'wireguard': 'WireGuard is an extremely simple yet fast and modern VPN that utilizes state-of-the-art cryptography.', 'nextcloud': 'Nextcloud is a suite of client-server software for creating and using file hosting services.', 'immich': 'High performance self-hosted photo and video backup solution.', 'paperless-ngx': 'Paperless-ngx is a document management system that transforms your physical documents into a searchable online archive.', 'gitea': 'Gitea is a community managed lightweight code hosting solution written in Go.', 'gitlab': 'GitLab is a web-based DevOps lifecycle tool that provides a Git-repository manager.', 'mattermost': 'Mattermost is an open-source, self-hostable online chat service with file sharing, search, and integrations.', 'matrix-synapse': 'Matrix Synapse is a reference homeserver implementation of the Matrix protocol.', 'mastodon': 'Mastodon is a free and open-source self-hosted social networking service.', 'minecraft': 'Minecraft server for multiplayer gaming.', 'factorio': 'Factorio dedicated server for multiplayer factory building.', 'satisfactory': 'Satisfactory dedicated server for multiplayer factory building in 3D.', 'ollama': 'Ollama is a tool for running large language models locally.', 'whisper': 'OpenAI Whisper is an automatic speech recognition system.', 'stable-diffusion': 'Stable Diffusion is a deep learning, text-to-image model.', } service_key = service_name.lower().replace('-', '').replace('_', '') # Try exact match first if service_key in descriptions: return descriptions[service_key] # Try partial matches for key, desc in descriptions.items(): if key in service_key or service_key in key: return desc # Generate generic description based on category category_descriptions = { 'media': f'{service_name} is a media management and streaming service that helps organize and serve your digital media content.', 'monitoring': f'{service_name} is a monitoring and observability tool that helps track system performance and health.', 'productivity': f'{service_name} is a productivity application that helps manage tasks, documents, or workflows.', 'development': f'{service_name} is a development tool that assists with code management, CI/CD, or software development workflows.', 'communication': f'{service_name} is a communication platform that enables messaging, collaboration, or social interaction.', 'security': f'{service_name} is a security tool that helps protect systems, manage authentication, or secure communications.', 'networking': f'{service_name} is a networking service that manages network traffic, routing, or connectivity.', 'storage': f'{service_name} is a storage solution that manages data persistence, backup, or file sharing.', 'gaming': f'{service_name} is a gaming server that hosts multiplayer games or gaming-related services.', 'ai': f'{service_name} is an AI/ML service that provides artificial intelligence or machine learning capabilities.', 'other': f'{service_name} is a specialized service that provides specific functionality for the homelab infrastructure.' } return category_descriptions.get(category, category_descriptions['other']) def format_compose_config(self, config: Dict) -> str: """Format the Docker Compose configuration for display.""" try: import yaml return yaml.dump(config, default_flow_style=False, indent=2) except: return str(config) def format_environment_variables(self, env_vars: Dict[str, str]) -> str: """Format environment variables for display.""" if not env_vars: return "No environment variables configured." result = "| Variable | Value | Description |\n|----------|-------|-------------|\n" for key, value in env_vars.items(): # Mask sensitive values display_value = value if any(sensitive in key.lower() for sensitive in ['password', 'secret', 'key', 'token']): display_value = '***MASKED***' result += f"| `{key}` | `{display_value}` | {self.get_env_var_description(key)} |\n" return result def get_env_var_description(self, var_name: str) -> str: """Get description for common environment variables.""" descriptions = { 'TZ': 'Timezone setting', 'PUID': 'User ID for file permissions', 'PGID': 'Group ID for file permissions', 'MYSQL_ROOT_PASSWORD': 'MySQL root password', 'POSTGRES_PASSWORD': 'PostgreSQL password', 'REDIS_PASSWORD': 'Redis authentication password', 'ADMIN_PASSWORD': 'Administrator password', 'SECRET_KEY': 'Application secret key', 'JWT_SECRET': 'JWT signing secret', 'DATABASE_URL': 'Database connection string', 'DOMAIN': 'Service domain name', 'BASE_URL': 'Base URL for the service', 'DEBUG': 'Enable debug mode', 'LOG_LEVEL': 'Logging verbosity level' } var_lower = var_name.lower() for key, desc in descriptions.items(): if key.lower() in var_lower: return desc return 'Configuration variable' def format_ports(self, ports: List[str]) -> str: """Format port mappings for display.""" if not ports: return "No ports exposed." result = "| Host Port | Container Port | Protocol | Purpose |\n|-----------|----------------|----------|----------|\n" for port in ports: if ':' in port: host_port, container_port = port.split(':', 1) protocol = 'TCP' if '/' in container_port: container_port, protocol = container_port.split('/') purpose = self.get_port_purpose(container_port) result += f"| {host_port} | {container_port} | {protocol.upper()} | {purpose} |\n" else: result += f"| {port} | {port} | TCP | Service port |\n" return result def get_port_purpose(self, port: str) -> str: """Get the purpose of common ports.""" port_purposes = { '80': 'HTTP web interface', '443': 'HTTPS web interface', '8080': 'Alternative HTTP port', '8443': 'Alternative HTTPS port', '3000': 'Web interface', '9000': 'Management interface', '5432': 'PostgreSQL database', '3306': 'MySQL/MariaDB database', '6379': 'Redis cache', '27017': 'MongoDB database', '9090': 'Prometheus metrics', '3001': 'Monitoring interface', '8086': 'InfluxDB', '25565': 'Minecraft server', '7777': 'Game server', '22': 'SSH access', '21': 'FTP', '53': 'DNS', '67': 'DHCP', '123': 'NTP', '161': 'SNMP', '514': 'Syslog', '1883': 'MQTT', '8883': 'MQTT over SSL' } return port_purposes.get(port, 'Service port') def format_volumes(self, volumes: List[str]) -> str: """Format volume mappings for display.""" if not volumes: return "No volumes mounted." result = "| Host Path | Container Path | Type | Purpose |\n|-----------|----------------|------|----------|\n" for volume in volumes: if ':' in volume: parts = volume.split(':') host_path = parts[0] container_path = parts[1] volume_type = 'bind' if host_path.startswith('/') or host_path.startswith('./') else 'volume' purpose = self.get_volume_purpose(container_path) result += f"| `{host_path}` | `{container_path}` | {volume_type} | {purpose} |\n" else: result += f"| `{volume}` | `{volume}` | volume | Data storage |\n" return result def get_volume_purpose(self, path: str) -> str: """Get the purpose of common volume paths.""" path_purposes = { '/config': 'Configuration files', '/data': 'Application data', '/app/data': 'Application data', '/var/lib': 'Service data', '/etc': 'Configuration files', '/logs': 'Log files', '/var/log': 'System logs', '/media': 'Media files', '/downloads': 'Downloaded files', '/uploads': 'Uploaded files', '/backup': 'Backup files', '/tmp': 'Temporary files', '/cache': 'Cache data', '/db': 'Database files', '/ssl': 'SSL certificates', '/keys': 'Encryption keys' } path_lower = path.lower() for key, purpose in path_purposes.items(): if key in path_lower: return purpose return 'Data storage' def generate_access_info(self, service_name: str, ports: List[str], host: str) -> str: """Generate access information for the service.""" if not ports: return "This service does not expose any web interfaces." web_ports = [] for port in ports: if ':' in port: host_port = port.split(':')[0] container_port = port.split(':')[1].split('/')[0] if container_port in ['80', '443', '8080', '8443', '3000', '9000', '8000', '5000']: web_ports.append(host_port) if not web_ports: return f"Service ports: {', '.join(ports)}" result = "### Web Interface\n" for port in web_ports: protocol = 'https' if port in ['443', '8443'] else 'http' result += f"- **{protocol.upper()}**: `{protocol}://{host}:{port}`\n" result += "\n### Default Credentials\n" result += self.get_default_credentials(service_name) return result def get_default_credentials(self, service_name: str) -> str: """Get default credentials for common services.""" credentials = { 'grafana': 'Username: `admin`, Password: "REDACTED_PASSWORD" (change on first login)', 'portainer': 'Set admin password on first access', 'jenkins': 'Check logs for initial admin password', 'gitlab': 'Username: `root`, Password: "REDACTED_PASSWORD" `/etc/gitlab/initial_root_password`', 'nextcloud': 'Set admin credentials during initial setup', 'mattermost': 'Create admin account during setup', 'mastodon': 'Create admin account via command line', 'matrix-synapse': 'Create users via command line or admin API', 'uptime-kuma': 'Set admin credentials on first access', 'vaultwarden': 'Create account on first access', 'paperless-ngx': 'Create superuser via management command' } service_key = service_name.lower().replace('-', '').replace('_', '') for key, creds in credentials.items(): if key in service_key or service_key in key: return creds return 'Refer to service documentation for default credentials' def generate_security_info(self, service_name: str, config: Dict) -> str: """Generate security information for the service.""" security_info = [] # Check for security options if 'security_opt' in config: security_info.append("āœ… Security options configured") else: security_info.append("āš ļø Consider adding security options (no-new-privileges)") # Check for user mapping if 'user' in config: security_info.append("āœ… Non-root user configured") else: security_info.append("āš ļø Consider running as non-root user") # Check for read-only root filesystem if config.get('read_only', False): security_info.append("āœ… Read-only root filesystem") # Check for capabilities if 'cap_drop' in config: security_info.append("āœ… Capabilities dropped") # Add service-specific security recommendations service_security = self.get_service_security_recommendations(service_name) if service_security: security_info.extend(service_security) return '\n'.join(f"- {info}" for info in security_info) def get_service_security_recommendations(self, service_name: str) -> List[str]: """Get security recommendations for specific services.""" recommendations = { 'vaultwarden': [ 'šŸ”’ Enable HTTPS with reverse proxy', 'šŸ”’ Disable user registration after setup', 'šŸ”’ Enable 2FA for all accounts', 'šŸ”’ Regular database backups' ], 'nextcloud': [ 'šŸ”’ Enable HTTPS', 'šŸ”’ Configure trusted domains', 'šŸ”’ Enable 2FA', 'šŸ”’ Regular security updates' ], 'gitlab': [ 'šŸ”’ Enable HTTPS', 'šŸ”’ Configure SSH keys', 'šŸ”’ Enable 2FA', 'šŸ”’ Regular backups' ], 'matrix-synapse': [ 'šŸ”’ Enable HTTPS', 'šŸ”’ Configure federation carefully', 'šŸ”’ Regular database backups', 'šŸ”’ Monitor resource usage' ] } service_key = service_name.lower().replace('-', '').replace('_', '') for key, recs in recommendations.items(): if key in service_key or service_key in key: return recs return [] def generate_resource_info(self, config: Dict) -> str: """Generate resource requirement information.""" resource_info = [] # Check for resource limits deploy_config = config.get('deploy', {}) resources = deploy_config.get('resources', {}) limits = resources.get('limits', {}) if limits: if 'memory' in limits: resource_info.append(f"**Memory Limit**: {limits['memory']}") if 'cpus' in limits: resource_info.append(f"**CPU Limit**: {limits['cpus']}") else: resource_info.append("No resource limits configured") # Add general recommendations resource_info.extend([ "", "### Recommended Resources", "- **Minimum RAM**: 512MB", "- **Recommended RAM**: 1GB+", "- **CPU**: 1 core minimum", "- **Storage**: Varies by usage", "", "### Resource Monitoring", "Monitor resource usage with:", "```bash", "docker stats", "```" ]) return '\n'.join(resource_info) def generate_health_info(self, config: Dict) -> str: """Generate health monitoring information.""" health_info = [] # Check for health check configuration if 'healthcheck' in config: health_config = config['healthcheck'] health_info.append("āœ… Health check configured") if 'test' in health_config: test_cmd = health_config['test'] if isinstance(test_cmd, list): test_cmd = ' '.join(test_cmd) health_info.append(f"**Test Command**: `{test_cmd}`") if 'interval' in health_config: health_info.append(f"**Check Interval**: {health_config['interval']}") if 'timeout' in health_config: health_info.append(f"**Timeout**: {health_config['timeout']}") if 'retries' in health_config: health_info.append(f"**Retries**: {health_config['retries']}") else: health_info.append("āš ļø No health check configured") health_info.append("Consider adding a health check:") health_info.append("```yaml") health_info.append("healthcheck:") health_info.append(" test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:PORT/health\"]") health_info.append(" interval: 30s") health_info.append(" timeout: 10s") health_info.append(" retries: 3") health_info.append("```") # Add monitoring commands health_info.extend([ "", "### Manual Health Checks", "```bash", "# Check container health", "docker inspect --format='{{.State.Health.Status}}' CONTAINER_NAME", "", "# View health check logs", "docker inspect --format='{{range .State.Health.Log}}{{.Output}}{{end}}' CONTAINER_NAME", "```" ]) return '\n'.join(health_info) def generate_troubleshooting_info(self, service_name: str, category: str) -> str: """Generate troubleshooting information.""" common_issues = [ "**Service won't start**", "- Check Docker logs: `docker-compose logs service-name`", "- Verify port availability: `netstat -tulpn | grep PORT`", "- Check file permissions on mounted volumes", "", "**Can't access web interface**", "- Verify service is running: `docker-compose ps`", "- Check firewall settings", "- Confirm correct port mapping", "", "**Performance issues**", "- Monitor resource usage: `docker stats`", "- Check available disk space: `df -h`", "- Review service logs for errors" ] # Add category-specific troubleshooting category_issues = { 'media': [ "", "**Media not showing**", "- Check media file permissions", "- Verify volume mounts are correct", "- Scan media library manually" ], 'monitoring': [ "", "**Metrics not collecting**", "- Check target endpoints are accessible", "- Verify configuration syntax", "- Check network connectivity" ], 'security': [ "", "**Authentication issues**", "- Verify credentials are correct", "- Check LDAP/SSO configuration", "- Review authentication logs" ] } issues = common_issues.copy() if category in category_issues: issues.extend(category_issues[category]) return '\n'.join(issues) def generate_additional_resources(self, service_name: str, image: str) -> str: """Generate additional resources section.""" resources = [ f"- **Official Documentation**: Check the official docs for {service_name}", f"- **Docker Hub**: [{image}](https://hub.docker.com/r/{image})" if '/' in image else f"- **Docker Hub**: [Official {service_name}](https://hub.docker.com/_/{image})", "- **Community Forums**: Search for community discussions and solutions", "- **GitHub Issues**: Check the project's GitHub for known issues" ] # Add service-specific resources service_resources = { 'plex': [ "- **Plex Support**: https://support.plex.tv/", "- **Plex Forums**: https://forums.plex.tv/" ], 'jellyfin': [ "- **Jellyfin Documentation**: https://jellyfin.org/docs/", "- **Jellyfin Forum**: https://forum.jellyfin.org/" ], 'grafana': [ "- **Grafana Documentation**: https://grafana.com/docs/", "- **Grafana Community**: https://community.grafana.com/" ], 'nextcloud': [ "- **Nextcloud Documentation**: https://docs.nextcloud.com/", "- **Nextcloud Community**: https://help.nextcloud.com/" ] } service_key = service_name.lower().replace('-', '').replace('_', '') for key, additional in service_resources.items(): if key in service_key or service_key in key: resources.extend(additional) break return '\n'.join(resources) def generate_related_services(self, service_name: str, category: str, host: str) -> str: """Generate related services information.""" # This would be populated with actual service relationships # For now, provide category-based suggestions category_related = { 'media': ['Plex', 'Jellyfin', 'Radarr', 'Sonarr', 'Bazarr', 'Tautulli'], 'monitoring': ['Grafana', 'Prometheus', 'Uptime Kuma', 'Node Exporter'], 'productivity': ['Nextcloud', 'Paperless-NGX', 'BookStack', 'Syncthing'], 'security': ['Vaultwarden', 'Authelia', 'Pi-hole', 'WireGuard'], 'development': ['GitLab', 'Gitea', 'Jenkins', 'Portainer'] } related = category_related.get(category, []) if not related: return f"Other services in the {category} category on {host}" return f"Services REDACTED_APP_PASSWORD {service_name}:\n" + '\n'.join(f"- {service}" for service in related[:5]) def get_current_date(self) -> str: """Get current date for documentation.""" from datetime import datetime return datetime.now().strftime("%Y-%m-%d") def generate_all_documentation(self): """Generate documentation for all services.""" print("šŸ” Finding Docker Compose files...") compose_files = self.find_compose_files() print(f"Found {len(compose_files)} compose files") all_services = {} print("šŸ“‹ Parsing service configurations...") for compose_file in compose_files: services = self.parse_compose_file(compose_file) all_services.update(services) print(f"Found {len(all_services)} total services") print("šŸ“ Generating individual service documentation...") for service_name, service_info in all_services.items(): print(f" Documenting {service_name}...") # Generate documentation content doc_content = self.generate_service_documentation(service_name, service_info) # Write to file doc_filename = f"{service_name.lower().replace('_', '-')}.md" doc_path = self.docs_path / doc_filename with open(doc_path, 'w', encoding='utf-8') as f: f.write(doc_content) print(f"āœ… Generated documentation for {len(all_services)} services") print(f"šŸ“ Documentation saved to: {self.docs_path}") # Generate index file self.generate_service_index(all_services) return len(all_services) def generate_service_index(self, all_services: Dict): """Generate an index file for all services.""" index_content = f"""# šŸ“š Individual Service Documentation Index This directory contains detailed documentation for all {len(all_services)} services in the homelab. ## šŸ“‹ Services by Category """ # Group services by category services_by_category = {} for service_name, service_info in all_services.items(): config = service_info['config'] image = config.get('image', '') category = self.categorize_service(service_name, image) if category not in services_by_category: services_by_category[category] = [] services_by_category[category].append({ 'name': service_name, 'host': service_info['host'], 'difficulty': self.get_difficulty_level(service_name, config) }) # Generate category sections for category in sorted(services_by_category.keys()): services = sorted(services_by_category[category], key=lambda x: x['name']) index_content += f"### {category.title()} ({len(services)} services)\n\n" for service in services: filename = f"{service['name'].lower().replace('_', '-')}.md" index_content += f"- {service['difficulty']} **[{service['name']}]({filename})** - {service['host']}\n" index_content += "\n" index_content += f""" ## šŸ“Š Statistics - **Total Services**: {len(all_services)} - **Categories**: {len(services_by_category)} - **Hosts**: {len(set(s['host'] for s in all_services.values()))} ## šŸ” Quick Search Use your browser's search function (Ctrl+F / Cmd+F) to quickly find specific services. --- *This index is auto-generated. Last updated: {self.get_current_date()}* """ # Write index file index_path = self.docs_path / "README.md" with open(index_path, 'w', encoding='utf-8') as f: f.write(index_content) print(f"šŸ“‹ Generated service index: {index_path}") if __name__ == "__main__": generator = ServiceDocumentationGenerator("/workspace/homelab") total_services = generator.generate_all_documentation() print(f"\nšŸŽ‰ Documentation generation complete!") print(f"šŸ“Š Total services documented: {total_services}")