diff --git a/stackbox/cli/__main__.py b/stackbox/cli/__main__.py index f44d3ea..a50e92d 100644 --- a/stackbox/cli/__main__.py +++ b/stackbox/cli/__main__.py @@ -1,6 +1,8 @@ """StackBox CLI entry point - class-based architecture.""" from pathlib import Path +import shutil +import subprocess import sys import time @@ -511,7 +513,36 @@ def init( click.echo(f"\nāŒ Failed to generate Tempest configuration: {e}", err=True) sys.exit(1) - # TODO: Phase 10 (Issue #12): Install Tempest and ironic-tempest-plugin + # Phase 10: Build Tempest container + click.echo("\n🐳 Building Tempest container...") + try: + # Copy Dockerfile to config directory + tempest_dockerfile_src = ( + Path(__file__).parent.parent / "templates" / "tempest" / "Dockerfile" + ) + tempest_dockerfile_dest = config_dir_path / "config" / "tempest" / "Dockerfile" + + shutil.copy2(tempest_dockerfile_src, tempest_dockerfile_dest) + click.echo(f" āœ… Copied: {tempest_dockerfile_dest.relative_to(Path.cwd())}") + + # Build Tempest image + click.echo(" šŸ”Ø Building Tempest image (this may take a few minutes)...") + + result = subprocess.run( + ["docker-compose", "build", "tempest"], + cwd=config_dir_path, + capture_output=True, + text=True, + ) + + if result.returncode != 0: + raise RuntimeError(f"Docker build failed: {result.stderr}") + + click.echo(" āœ… Tempest image built successfully") + + except Exception as e: + click.echo(f"\nāŒ Failed to build Tempest container: {e}", err=True) + sys.exit(1) click.echo("\nāœ… Initialization complete!") click.echo(" Ironic API: http://localhost:6385") @@ -519,6 +550,8 @@ def init( click.echo(" BMC endpoint: http://localhost:8000/redfish/v1/") click.echo(f" Enrolled in Ironic: {enrolled_node['uuid']}") click.echo(f" Tempest config: {tempest_conf.relative_to(Path.cwd())}") + click.echo("\nšŸ’” Ready to run tests:") + click.echo(" sb test # Run all Tempest tests") def rebuild(self, service: str, no_cache: bool) -> None: """Rebuild a service after code changes.""" diff --git a/stackbox/templates/docker-compose.yml.j2 b/stackbox/templates/docker-compose.yml.j2 index dfb8282..2a250e6 100644 --- a/stackbox/templates/docker-compose.yml.j2 +++ b/stackbox/templates/docker-compose.yml.j2 @@ -141,6 +141,25 @@ services: start_period: 30s restart: unless-stopped + # Tempest test runner - only started on demand via profiles + tempest: + build: + context: . + dockerfile: ./config/tempest/Dockerfile + container_name: stackbox-tempest + profiles: + - testing + volumes: + - ./config/tempest:/etc/tempest:ro + - tempest-results:/tmp/tempest-results + depends_on: + ironic-api: + condition: service_healthy + networks: + - stackbox + environment: + - TEMPEST_CONFIG_DIR=/etc/tempest + volumes: mariadb-data: name: stackbox-mariadb-data @@ -152,6 +171,8 @@ volumes: name: stackbox-ironic-lib ironic-log: name: stackbox-ironic-log + tempest-results: + name: stackbox-tempest-results networks: stackbox: diff --git a/stackbox/templates/tempest/Dockerfile b/stackbox/templates/tempest/Dockerfile new file mode 100644 index 0000000..90128af --- /dev/null +++ b/stackbox/templates/tempest/Dockerfile @@ -0,0 +1,46 @@ +# Tempest container for testing Ironic +FROM ubuntu:22.04 + +# Prevent interactive prompts during apt install +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED=1 + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + python3 \ + python3-pip \ + python3-dev \ + git \ + gcc \ + make \ + build-essential \ + libssl-dev \ + libffi-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Upgrade pip to latest version +RUN pip3 install --no-cache-dir --upgrade pip setuptools wheel + +# Install Tempest framework +RUN pip3 install --no-cache-dir \ + tempest + +# Install ironic-tempest-plugin from git +# NOTE: Using git URL to get latest version compatible with our Ironic setup +RUN pip3 install --no-cache-dir \ + git+https://opendev.org/openstack/ironic-tempest-plugin.git + +# Create Tempest directories +RUN mkdir -p \ + /etc/tempest \ + /tmp/tempest-results + +# Set working directory to Tempest config location +WORKDIR /etc/tempest + +# Tempest entry point - allows flexible command invocation +ENTRYPOINT ["tempest"] + +# Default command shows help +CMD ["--help"] diff --git a/tests/unit/core/test_compose.py b/tests/unit/core/test_compose.py index 251043d..61efd8c 100644 --- a/tests/unit/core/test_compose.py +++ b/tests/unit/core/test_compose.py @@ -123,7 +123,10 @@ def test_libvirt_uses_containerized_uri(self, tmp_path: Path) -> None: assert "qemu:///system" not in libvirt_uri def test_health_checks_configured(self, tmp_path: Path) -> None: - """Test that all services have health checks.""" + """Test that all long-running services have health checks. + + Services with profiles (on-demand tools like tempest) don't need health checks. + """ output = tmp_path / "docker-compose.yml" generate_compose_file(output) @@ -131,6 +134,10 @@ def test_health_checks_configured(self, tmp_path: Path) -> None: data = yaml.safe_load(f) for service_name, service in data["services"].items(): + # Skip services with profiles (on-demand tools, not long-running services) + if "profiles" in service: + continue + assert "healthcheck" in service, f"{service_name} missing healthcheck" assert "test" in service["healthcheck"]