"""Integration: the Ansible foundation (task 545). Proves the two foundation pieces against a live Postgres + a real ansible install: 1. ansible-playbook is on PATH (ships in the image / [ansible] extra). 2. executor.start_run actually runs a trivial connection:local playbook end to end and records success + output on the AnsibleRun row. 3. An AnsibleRun with triggered_by=NULL inserts and round-trips (migration 0012). Requires STEWARD_DATABASE_URL (the integration CI lane provides it). A fresh app is built per test so each test's async work runs on its own event loop with its own engine (create_app's internal asyncio.run can't be nested under a running loop, so these tests are sync and drive async work via asyncio.run). """ from __future__ import annotations import asyncio import os import shutil import textwrap import uuid import pytest pytestmark = pytest.mark.integration _NEEDS_DB = pytest.mark.skipif( not os.environ.get("STEWARD_DATABASE_URL"), reason="integration test needs a live Postgres (STEWARD_DATABASE_URL)", ) @pytest.fixture def app(): if not os.environ.get("STEWARD_DATABASE_URL"): pytest.skip("needs Postgres") from steward.app import create_app # testing=False → runs all migrations (incl. 0012) and gives a real engine. return create_app(testing=False) def test_ansible_playbook_on_path(): assert shutil.which("ansible-playbook") is not None @_NEEDS_DB def test_null_triggered_run_inserts(app): from sqlalchemy import select from steward.models.ansible import AnsibleRun, AnsibleRunStatus run_id = str(uuid.uuid4()) async def _go(): async with app.db_sessionmaker() as s: async with s.begin(): s.add(AnsibleRun( id=run_id, playbook_path="p.yml", inventory_path="i.ini", source_name="t", triggered_by=None, status=AnsibleRunStatus.success, )) async with app.db_sessionmaker() as s: row = (await s.execute( select(AnsibleRun).where(AnsibleRun.id == run_id))).scalar_one() return row.triggered_by assert asyncio.run(_go()) is None @_NEEDS_DB def test_executor_runs_local_playbook(app, tmp_path): from sqlalchemy import select from steward.ansible import executor from steward.models.ansible import AnsibleRun, AnsibleRunStatus (tmp_path / "test.yml").write_text(textwrap.dedent("""\ - hosts: localhost connection: local gather_facts: false tasks: - debug: msg: STEWARD_ANSIBLE_OK """)) (tmp_path / "inv.ini").write_text("localhost ansible_connection=local\n") run_id = str(uuid.uuid4()) async def _go(): async with app.db_sessionmaker() as s: async with s.begin(): s.add(AnsibleRun( id=run_id, playbook_path="test.yml", inventory_path="inv.ini", source_name="t", triggered_by=None, status=AnsibleRunStatus.running, )) await executor.start_run(app, run_id, "test.yml", "inv.ini", str(tmp_path)) async with app.db_sessionmaker() as s: return (await s.execute( select(AnsibleRun).where(AnsibleRun.id == run_id))).scalar_one() run = asyncio.run(_go()) assert run.status == AnsibleRunStatus.success, run.output assert "STEWARD_ANSIBLE_OK" in (run.output or "") @_NEEDS_DB def test_executor_runs_parameterized(app, tmp_path): """extra_vars + limit + check all flow through to a real run (task 546).""" from sqlalchemy import select from steward.ansible import executor from steward.models.ansible import AnsibleRun, AnsibleRunStatus (tmp_path / "test.yml").write_text(textwrap.dedent("""\ - hosts: localhost connection: local gather_facts: false tasks: - debug: msg: "GOT-{{ myvar }}" """)) (tmp_path / "inv.ini").write_text("localhost ansible_connection=local\n") run_id = str(uuid.uuid4()) params = {"extra_vars": ["myvar=HELLO"], "limit": "localhost", "check": True} async def _go(): async with app.db_sessionmaker() as s: async with s.begin(): s.add(AnsibleRun( id=run_id, playbook_path="test.yml", inventory_path="inv.ini", source_name="t", triggered_by=None, status=AnsibleRunStatus.running, params=params, )) await executor.start_run(app, run_id, "test.yml", "inv.ini", str(tmp_path), params) async with app.db_sessionmaker() as s: return (await s.execute( select(AnsibleRun).where(AnsibleRun.id == run_id))).scalar_one() run = asyncio.run(_go()) assert run.status == AnsibleRunStatus.success, run.output assert "GOT-HELLO" in (run.output or "") # extra var substituted assert run.params == params # params round-trip through the DB