fix(host_agent): disks behind a RAID controller each keep their own name (#5806)
smartctl reports every disk behind a PERC/MegaRAID controller as /dev/bus/0 with type megaraid,N; the helper named a drive by its path alone, so all of VDNT-Docker02's and Docker03's disks read as one drive "0", each overwriting the last. The slot is now part of the name (bus0-megaraid3), and Steward gives a repeated name an ordinal so two drives never share a series. Agent 1.20.1. fix(ansible): Steward stopping mid-run logs no traceback (#5749) The run's done-callback called exception() on a cancelled task, which raises; it now logs a run cut short by shutdown at INFO. A finished run's in-memory state is dropped by a timer rather than a sleeping task, which shutdown found still pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -80,6 +80,38 @@ def test_a_drive_with_no_smart_and_one_that_can_t_be_opened_say_so(maint):
|
||||
assert maint.drive_summary("/dev/sdd", None, None)["error"] == "smartctl couldn't read it"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name, kind, device", [
|
||||
("/dev/sda", "sat", "sda"),
|
||||
("/dev/nvme0", "nvme", "nvme0"),
|
||||
("/dev/bus/0", "megaraid,3", "bus0-megaraid3"), # VDNT-Docker02's PERC (#5806)
|
||||
("/dev/bus/0", "sat+megaraid,12", "bus0-megaraid12"),
|
||||
("/dev/sg1", "cciss,0", "sg1-cciss0"),
|
||||
("/dev/sdc", None, "sdc"),
|
||||
])
|
||||
def test_each_drive_has_a_name_of_its_own(maint, name, kind, device):
|
||||
assert maint.drive_name(name, kind) == device
|
||||
|
||||
|
||||
def test_disks_behind_one_controller_stay_apart(maint, monkeypatch):
|
||||
def fake(args):
|
||||
if args == ["--scan-open"]:
|
||||
return 0, {"devices": [{"name": "/dev/bus/0", "type": "megaraid,0"},
|
||||
{"name": "/dev/bus/0", "type": "megaraid,1"}]}
|
||||
return 0, ATA
|
||||
monkeypatch.setattr(maint, "smartctl_version", lambda: (7, 4))
|
||||
monkeypatch.setattr(maint, "smartctl_json", fake)
|
||||
assert [d["device"] for d in maint.do_drives()["drives"]] == ["bus0-megaraid0", "bus0-megaraid1"]
|
||||
|
||||
|
||||
def test_a_name_seen_twice_is_still_two_drives():
|
||||
"""What agent 1.20.0 sent from a PERC: every disk named "0"."""
|
||||
report = {"drives": [{"device": "0", "passed": True}, {"device": "0", "passed": False},
|
||||
{"device": "0", "passed": True}]}
|
||||
assert sorted(d["device"] for d in dh.drives_of(report)) == ["0", "0#2", "0#3"]
|
||||
rows = {(m, r) for m, r, _ in dh.metric_rows(report, "box") if m == "drive_failing"}
|
||||
assert len(rows) == 3
|
||||
|
||||
|
||||
def test_a_failing_disk_s_figures_still_count(maint):
|
||||
"""Exit bit 3 says the disk is failing; its JSON is the point, not an error."""
|
||||
failing = {**ATA, "smart_status": {"passed": False}}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""#5749: a redeploy during an Ansible run logged two CancelledError tracebacks
|
||||
from the run's done-callback and two "Task was destroyed but it is pending!"
|
||||
lines for its in-memory cleanup. Steward stopping mid-run is not an error: the
|
||||
next start marks the run interrupted."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from steward.ansible import executor
|
||||
from steward.ansible.runner import run_ended
|
||||
|
||||
|
||||
class _App:
|
||||
logger = logging.getLogger("test.ansible.run_end")
|
||||
|
||||
|
||||
async def test_a_run_cut_short_by_shutdown_logs_no_error(caplog):
|
||||
async def run():
|
||||
await asyncio.sleep(60)
|
||||
|
||||
task = asyncio.create_task(run())
|
||||
task.add_done_callback(run_ended(_App, "r1"))
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
with caplog.at_level(logging.INFO, logger="test.ansible.run_end"):
|
||||
await asyncio.gather(task, return_exceptions=True)
|
||||
await asyncio.sleep(0) # done-callbacks run on the next turn
|
||||
assert [r.levelname for r in caplog.records] == ["INFO"]
|
||||
assert "Ansible run r1 interrupted" in caplog.records[0].getMessage()
|
||||
|
||||
|
||||
async def test_a_run_that_raised_is_still_an_error(caplog):
|
||||
async def run():
|
||||
raise RuntimeError("ansible-playbook vanished")
|
||||
|
||||
task = asyncio.create_task(run())
|
||||
task.add_done_callback(run_ended(_App, "r2"))
|
||||
with caplog.at_level(logging.INFO, logger="test.ansible.run_end"):
|
||||
await asyncio.gather(task, return_exceptions=True)
|
||||
await asyncio.sleep(0)
|
||||
assert [(r.levelname, r.getMessage()) for r in caplog.records] == [
|
||||
("ERROR", "Ansible run r2 raised: ansible-playbook vanished")]
|
||||
|
||||
|
||||
async def test_a_finished_run_s_state_is_dropped_by_a_timer_not_a_task(monkeypatch):
|
||||
"""A task left sleeping is what shutdown found pending; a timer isn't one."""
|
||||
monkeypatch.setattr(executor, "_MEM_TTL_SECS", 0.01)
|
||||
executor._run_lines["r3"] = ["ok"]
|
||||
before = asyncio.all_tasks()
|
||||
executor._schedule_cleanup("r3")
|
||||
assert asyncio.all_tasks() == before
|
||||
await asyncio.sleep(0.05)
|
||||
assert "r3" not in executor._run_lines
|
||||
Reference in New Issue
Block a user