fix(ansible): Run now runs the playbook and leaves the schedule's time alone (#5445)
Run now set last_run_at to the click, and a schedule's next turn counts from last_run_at, so a manual run of the nightly prune at 14:00 moved every later run to 14:00. Operator ruling: "run now shouldn't change the schedule it should just run the playbook and leave the schedule as it was." Run now no longer touches last_run_at. It still records the run as last_run_id, so the list shows it and a firing during it is skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -743,12 +743,16 @@ async def run_schedule_now(schedule_id: str):
|
||||
inventory_scope=inventory_scope, params=params,
|
||||
triggered_by=session["user_id"], origin="schedule", origin_id=schedule_id,
|
||||
)
|
||||
# Run now runs the playbook and leaves the schedule where it was: its
|
||||
# time of day (last_run_at, the slot its next turn counts from) is not
|
||||
# touched (operator, 2026-10-09). The run is still the schedule's latest,
|
||||
# so the list shows it and a firing while it goes is skipped (#5158).
|
||||
async with current_app.db_sessionmaker() as db:
|
||||
async with db.begin():
|
||||
fresh = await db.get(AnsibleSchedule, schedule_id)
|
||||
if fresh:
|
||||
fresh.last_run_at = datetime.now(timezone.utc)
|
||||
fresh.last_run_id = run.id if run else None
|
||||
if run:
|
||||
fresh.last_run_id = run.id
|
||||
fresh.last_error = err
|
||||
await _audit("ansible.schedule.run_now", "ansible_schedule", schedule_id,
|
||||
{"playbook": playbook_path, "run_id": run.id if run else None, "error": err})
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Integration: Run now on a schedule runs its playbook and leaves the
|
||||
schedule where it was (operator, 2026-10-09): its time of day stays, and the
|
||||
run it started is the one the list shows."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.integration
|
||||
|
||||
CSRF = {"X-CSRF-Token": "test-token"}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
if not os.environ.get("STEWARD_DATABASE_URL"):
|
||||
pytest.skip("needs Postgres")
|
||||
from steward.app import create_app
|
||||
return create_app(testing=False)
|
||||
|
||||
|
||||
def test_run_now_leaves_the_schedule_s_time_alone(app, monkeypatch):
|
||||
from steward.ansible import runner
|
||||
from steward.models.ansible_schedule import AnsibleSchedule
|
||||
from steward.models.users import User, UserRole
|
||||
|
||||
started: list[dict] = []
|
||||
run_id = str(uuid.uuid4())
|
||||
|
||||
async def fake_trigger(_app, **kw):
|
||||
started.append(kw)
|
||||
return SimpleNamespace(id=run_id), None, None
|
||||
|
||||
monkeypatch.setattr(runner, "trigger_run", fake_trigger)
|
||||
|
||||
async def _go():
|
||||
tag = uuid.uuid4().hex[:8]
|
||||
uid, sid = str(uuid.uuid4()), str(uuid.uuid4())
|
||||
slot = datetime.now(timezone.utc).replace(microsecond=0) - timedelta(hours=3)
|
||||
async with app.db_sessionmaker() as s:
|
||||
async with s.begin():
|
||||
s.add(User(id=uid, username=f"rn-{tag}", email=f"rn-{tag}@example.com",
|
||||
password_hash="x", role=UserRole.operator, is_active=True))
|
||||
s.add(AnsibleSchedule(id=sid, name=f"nightly-{tag}", source_name="steward-builtin",
|
||||
playbook_path="maintenance/docker_prune.yml",
|
||||
interval_seconds=86400, enabled=True, last_run_at=slot))
|
||||
client = app.test_client()
|
||||
async with client.session_transaction() as sess:
|
||||
sess.update(user_id=uid, user_role="operator", username=f"rn-{tag}",
|
||||
_csrf="test-token", login_at=time.time())
|
||||
|
||||
resp = await client.post(f"/ansible/schedules/{sid}/run-now", headers=CSRF)
|
||||
assert resp.status_code == 302 and resp.headers["Location"].endswith(run_id)
|
||||
assert [kw["origin_id"] for kw in started] == [sid]
|
||||
async with app.db_sessionmaker() as s:
|
||||
sched = await s.get(AnsibleSchedule, sid)
|
||||
assert sched.last_run_at == slot # its next turn is still a day after the slot
|
||||
assert sched.last_run_id == run_id and sched.last_error is None
|
||||
|
||||
asyncio.run(_go())
|
||||
Reference in New Issue
Block a user