ruff 0.16 enables rules that earlier releases did not, so `ruff check .`
was already failing on HEAD with 35 findings - the CI job installs ruff
unpinned and would have gone red on the next push regardless of this
branch.
All changes are mechanical and behaviour-neutral:
- Optional[X] -> X | None, List[str] -> list[str], dropped the now unused
typing imports (every module already carries
`from __future__ import annotations`, and requires-python is >= 3.10).
- Sorted import blocks, `import automtu.x as x` -> `from automtu import x`.
- subprocess.run in pmtu.py and wg.py now passes check=False explicitly.
That is the parameter default, so the calls behave exactly as before.
- int(round(x)) -> round(x): round() with a single argument already
returns int.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/sys/class/net/<if> is always a symlink into /sys/devices; in nested
containers (sysbox runtime) that target is not visible, so the link is
dead. iface_exists() used Path.exists(), which follows the link and
therefore tested the visibility of the target instead of the existence of
the interface. detect_egress_iface() discarded every interface it had
correctly read from the routing table, so automtu aborted with "Could not
detect egress interface" and rc=2 even though `ip -4 route show default`
and `ip link show dev eth0` both worked. The documented escape hatch
--egress-if was equally dead, because core.py validates it through the
same call.
Chosen fix: os.path.lexists() rather than probing netlink for existence.
It asks the right question -- "is there an entry named <if>" -- and costs
no subprocess on a healthy host. Netlink (`ip link show`) is only the
fallback for when /sys/class/net itself is unavailable, so sysfs stays the
preferred path everywhere.
read_iface_mtu() gains the same cascade: sysfs first, MTU parsed from
`ip link show dev <if>` when the sysfs path is unreadable, RuntimeError
naming both sources when neither answers - no silent default. core.py
turns that into an error line plus rc=3 instead of a traceback.
list_ifaces() had the same defect (is_dir() on a dead symlink) and would
have left Docker bridge detection blind in the same environments.
Regression tests build a /sys replacement whose class/net/eth0 points at a
missing target and mock the ip command: they fail if lexists becomes
exists again, if the MTU fallback is removed, or if the symlink filter is
dropped.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>