Add interactive CLI setup wizard for Grafana scenarios
- Implements clean class-based architecture with modern Python typing - Supports 5 scenarios: standalone Grafana, Prometheus, Loki, Tempo, Pyroscope - Docker version checking with graceful fallback - Uses questionary for Vue/Vite-like aesthetics - Runnable with: uv run setup_wizard.py 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
197
setup_wizard.py
Normal file
197
setup_wizard.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "questionary==2.1.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import webbrowser
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import questionary
|
||||
|
||||
|
||||
@dataclass
|
||||
class Scenario:
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
|
||||
def run(self, base_dir: Path) -> None:
|
||||
"""Execute this scenario."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class StandaloneGrafana(Scenario):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="01",
|
||||
name="Standalone Grafana",
|
||||
description="Vanilla Grafana instance"
|
||||
)
|
||||
|
||||
def run(self, base_dir: Path) -> None:
|
||||
print(f"\n→ Starting {self.name}...")
|
||||
print("Access at: http://localhost:3000 (admin/admin)")
|
||||
|
||||
subprocess.run([
|
||||
"docker", "run", "-i", "-t", "--rm", "-p", "3000:3000",
|
||||
"grafana/grafana:12.0.2"
|
||||
], check=True)
|
||||
|
||||
|
||||
class ComposeBased(Scenario):
|
||||
"""Base class for docker-compose based scenarios."""
|
||||
|
||||
def run(self, base_dir: Path) -> None:
|
||||
scenario_dir = base_dir / self.id
|
||||
if not scenario_dir.exists():
|
||||
print(f"Directory '{self.id}' not found")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"\n→ Starting {self.name}...")
|
||||
print("Access at: http://localhost:3000 (admin/admin)")
|
||||
|
||||
subprocess.run(["docker-compose", "up"], cwd=scenario_dir, check=True)
|
||||
|
||||
|
||||
class PrometheusSetup(ComposeBased):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="02",
|
||||
name="Grafana + Prometheus",
|
||||
description="Basic metrics collection"
|
||||
)
|
||||
|
||||
|
||||
class LokiSetup(ComposeBased):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="03",
|
||||
name="Grafana + Loki",
|
||||
description="Log aggregation and exploration"
|
||||
)
|
||||
|
||||
|
||||
class TempoSetup(ComposeBased):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="04",
|
||||
name="Grafana + Tempo",
|
||||
description="Distributed tracing"
|
||||
)
|
||||
|
||||
|
||||
class PyroscopeSetup(ComposeBased):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
id="05",
|
||||
name="Grafana + Pyroscope",
|
||||
description="Continuous profiling"
|
||||
)
|
||||
|
||||
|
||||
def check_docker() -> tuple[bool, str | None]:
|
||||
"""Check if Docker is installed and return version info."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["docker", "--version"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return True, result.stdout.strip()
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return False, None
|
||||
|
||||
|
||||
def handle_missing_docker() -> None:
|
||||
"""Handle case when Docker is not installed."""
|
||||
choice = questionary.select(
|
||||
"Docker is required. What would you like to do?",
|
||||
choices=[
|
||||
"Open installation page",
|
||||
"Exit"
|
||||
]
|
||||
).ask()
|
||||
|
||||
if choice == "Open installation page":
|
||||
webbrowser.open("https://docs.docker.com/get-docker/")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def get_scenarios() -> list[Scenario]:
|
||||
"""Return list of available scenarios."""
|
||||
return [
|
||||
StandaloneGrafana(),
|
||||
PrometheusSetup(),
|
||||
LokiSetup(),
|
||||
TempoSetup(),
|
||||
PyroscopeSetup()
|
||||
]
|
||||
|
||||
|
||||
def select_scenario() -> Scenario | None:
|
||||
"""Display scenario selection menu and return choice."""
|
||||
scenarios = get_scenarios()
|
||||
|
||||
print("\nGrafana Setup Wizard")
|
||||
print("Choose a scenario:")
|
||||
|
||||
choices = [f"{s.id} · {s.name} – {s.description}" for s in scenarios]
|
||||
|
||||
selected = questionary.select("Select scenario:", choices=choices).ask()
|
||||
|
||||
if not selected:
|
||||
return None
|
||||
|
||||
scenario_id = selected[:2]
|
||||
return next(s for s in scenarios if s.id == scenario_id)
|
||||
|
||||
|
||||
def run_scenario(scenario: Scenario) -> None:
|
||||
"""Execute the selected scenario with proper error handling."""
|
||||
base_dir = Path(__file__).parent
|
||||
|
||||
try:
|
||||
scenario.run(base_dir)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt:
|
||||
print(f"\n→ Stopping {scenario.name}...")
|
||||
|
||||
if scenario.id != "01":
|
||||
scenario_dir = base_dir / scenario.id
|
||||
try:
|
||||
subprocess.run(["docker-compose", "down"], cwd=scenario_dir)
|
||||
print("Services stopped")
|
||||
except subprocess.CalledProcessError:
|
||||
print("Some services may still be running")
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main function."""
|
||||
docker_available, docker_version = check_docker()
|
||||
|
||||
if not docker_available:
|
||||
handle_missing_docker()
|
||||
return
|
||||
|
||||
print(f"✓ {docker_version}")
|
||||
|
||||
scenario = select_scenario()
|
||||
if scenario:
|
||||
run_scenario(scenario)
|
||||
else:
|
||||
print("No scenario selected")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
Reference in New Issue
Block a user