Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 73 additions & 18 deletions stackbox/cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from requests import Timeout as RequestsTimeout

from stackbox import __version__
from stackbox.core import builder
from stackbox.core import builder, tempest


class StackBoxCLI:
Expand Down Expand Up @@ -130,7 +130,10 @@ def _create_test_command(self) -> click.Command:
@click.command(name="test")
@click.option("--regex", help="Test regex pattern to run (e.g., test_baremetal_basic_ops)")
@click.option("--verbose", "-v", is_flag=True, help="Show verbose output")
def test_cmd(regex: str | None, verbose: bool) -> None:
@click.option(
"--list", "list_tests", is_flag=True, help="List available tests instead of running"
)
def test_cmd(regex: str | None, verbose: bool, list_tests: bool) -> None:
"""Run Tempest tests.

Runs ironic-tempest-plugin tests in the environment.
Expand All @@ -139,8 +142,9 @@ def test_cmd(regex: str | None, verbose: bool) -> None:
sb test
sb test --regex test_baremetal_basic_ops
sb test --verbose
sb test --list
"""
self.test(regex, verbose)
self.test(regex, verbose, list_tests)

return test_cmd

Expand Down Expand Up @@ -495,14 +499,25 @@ def init(
# BMC address for container-to-container communication
bmc_address = "http://sushy-tools:8000"

# Enroll node
enrolled_node = enrollment.enroll_node(
ironic_client,
node_info,
bmc_address,
)
# Check if node already exists
existing_nodes = ironic_client.list_nodes()
node_names = [n.get("name") for n in existing_nodes]

click.echo(f"✅ Node enrolled: {enrolled_node['uuid']}")
if node_info["name"] in node_names:
click.echo(
f"ℹ️ Node '{node_info['name']}' already exists in Ironic" # noqa: RUF001
)
# Get existing node details
enrolled_node = ironic_client.get_node(node_info["name"])
click.echo(f" Using existing node: {enrolled_node['uuid']}")
else:
# Enroll new node
enrolled_node = enrollment.enroll_node(
ironic_client,
node_info,
bmc_address,
)
click.echo(f"✅ Node enrolled: {enrolled_node['uuid']}")

# Verify power control
click.echo("🔌 Verifying power control...")
Expand Down Expand Up @@ -545,6 +560,11 @@ def init(
config_gen.generate_tempest_accounts(output_path=accounts_file)
click.echo(f" ✅ Generated: {accounts_file.relative_to(config_dir_path)}")

# Generate .stestr.conf
stestr_conf = config_dir_path / "config" / "tempest" / ".stestr.conf"
config_gen.generate_stestr_conf(output_path=stestr_conf)
click.echo(f" ✅ Generated: {stestr_conf.relative_to(config_dir_path)}")

# Validate configuration
if config_gen.validate_tempest_config(tempest_conf):
click.echo(" ✅ Configuration validated")
Expand Down Expand Up @@ -608,17 +628,52 @@ def rebuild(self, service: str, no_cache: bool) -> None:
click.echo(" Using --no-cache")
click.echo("\n⚠️ Not implemented yet (Issue #16)")

def test(self, regex: str | None, verbose: bool) -> None:
def test(self, regex: str | None, verbose: bool, list_tests: bool = False) -> None:
"""Run Tempest tests."""
if regex:
click.echo(f"Running tests matching: {regex}")
else:
click.echo("Running all tests...")
config_dir = Path(".stackbox")

if verbose:
click.echo(" Verbose mode enabled")
# Verify environment is set up
tempest_conf = config_dir / "config" / "tempest" / "tempest.conf"
if not tempest_conf.exists():
click.echo("❌ Tempest not configured. Run 'sb init' first.", err=True)
sys.exit(1)

click.echo("\n⚠️ Not implemented yet (Issue #13)")
# Verify Ironic is running (basic check)
try:
import requests

resp = requests.get("http://localhost:6385/", timeout=5)
if resp.status_code != 200:
click.echo("⚠️ Ironic API returned unexpected status", err=True)
except requests.exceptions.RequestException:
click.echo("❌ Ironic API not accessible. Is the environment running?", err=True)
click.echo(" Try: docker ps", err=True)
sys.exit(1)

# List tests if requested
if list_tests:
try:
tests = tempest.list_available_tests(config_dir, regex=regex or "baremetal")
click.echo(f"\n📝 Available tests ({len(tests)}):\n")
for test_name in tests:
click.echo(f" • {test_name}")
sys.exit(0)
except Exception as e:
click.echo(f"\n❌ Failed to list tests: {e}", err=True)
sys.exit(1)

# Run tests
try:
success = tempest.run_tests(config_dir, regex=regex, verbose=verbose)

# Show summary
tempest.show_test_summary(config_dir)

sys.exit(0 if success else 1)

except Exception as e:
click.echo(f"\n❌ Test execution failed: {e}", err=True)
sys.exit(1)

def status(self, json: bool) -> None:
"""Show environment status."""
Expand Down
19 changes: 19 additions & 0 deletions stackbox/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,25 @@ def generate_tempest_accounts(
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(accounts_content)

def generate_stestr_conf(
self,
output_path: Path,
) -> None:
"""Generate .stestr.conf for Tempest test runner.

Args:
output_path: Where to write .stestr.conf
"""
# Hard-coded configuration - stestr conf doesn't use Jinja2 templating
stestr_content = r"""[DEFAULT]
test_path=/usr/local/lib/python3.10/dist-packages/tempest/test_discover
top_dir=/usr/local/lib/python3.10/dist-packages/tempest
group_regex=([^\.]*).*
"""

output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(stestr_content)

def validate_tempest_config(self, config_path: Path) -> bool:
"""Validate that Tempest configuration has required sections.

Expand Down
Loading
Loading