Sanitized mirror from private repository - 2026-03-17 11:52:42 UTC
This commit is contained in:
928
scripts/generate_service_docs.py
Normal file
928
scripts/generate_service_docs.py
Normal file
@@ -0,0 +1,928 @@
|
||||
#!/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}")
|
||||
Reference in New Issue
Block a user