The RPM workflows rsync packages to rpm/master/<family>/<rel>/<arch>/
on zmrepo but never run createrepo_c, so that tree has no repodata and
dnf can't use it.
Add utils/zmrepo_createrepo.sh and run it over ssh after the publish
step. It serializes runs per directory with flock, removes a .repodata/
temp dir left by a run that died (createrepo_c refuses to start while
one exists; that is what froze el/10/x86_64 and fc/42/x86_64 in the
older tree on 2026-06-18), and ignores hangups so cancelling the job
doesn't kill createrepo_c halfway and leave the same mess behind.
The zmrepo host needs createrepo_c and flock installed.
tests/python/test_zmrepo_createrepo.py drives the script through
bash -s with a stub createrepo_c: stale temp dir cleanup, exit codes,
lock timeout, and surviving SIGHUP mid-run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: validate alarm packets in zmalarm-server.py
The alarm server accepted any packet from any host and mapped it to a
monitor by the Address field inside the packet, so a host on the network
could raise an alarm or a zmtrigger event on any configured camera.
Every malformed packet (short header, non-JSON body, unknown address)
raised an uncaught exception that ended the process, and a client that
connected without sending blocked the accept loop indefinitely.
- only accept an alarm whose Address matches the address it came from
and a monitor that uses that address
- read the header and body with a 5 s timeout, cap the body at 64 KiB,
log and drop bad packets instead of exiting
- resolve camera hostnames in monitor Paths and skip monitors without a
camera address instead of failing at startup
- pass the alarm description to the zmtrigger thread instead of reading
a global, and talk to zmtrigger over a plain socket (telnetlib was
removed in Python 3.13)
- honour the bare port number zmpkg.pl passes from ZM_OPT_ALS_PORT and
add --bind= to choose the listening address (default unchanged)
- move the pyzm imports into main() so the parser can be tested; add
tests/python/test_zmalarm_server.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: accept alarms from cameras behind NAT in zmalarm-server.py
The source check required the packet's Address field to equal the address
the packet came from. A camera behind NAT reports its LAN address there
while its packets arrive from the NAT address, so its alarms were dropped.
The Address field is no credential, so the monitor is now chosen by the
sender: an alarm is accepted only from an address that belongs to a
monitor, the camera address in its Path or one listed for it under
[alarmserver] in secrets.ini, and it goes to one of that address's
monitors. The Address field only picks among several monitors on one
address, for cameras behind a shared NAT address. A host that belongs to no
monitor still cannot raise an alarm, and a camera cannot raise one on
another camera's monitor.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix: cancel the forced alarms zmalarm-server.py raised when it stops
An alarm packet armed the monitor, slept 60 s in a thread and disarmed it.
When zmdc stopped or restarted the server inside that window (SIGTERM, which
Python's default handler ends at once) the disarm never ran and the monitor
stayed in forced alarm, recording, until someone cancelled it by hand.
Keep the forced alarms the server raised in one place, one per monitor: a
new alarm for a monitor in forced alarm arms it again, as every packet did,
and moves its end instead of starting a second timer that would cut it
short. On the way out, SIGTERM included, the server cancels every alarm
still running and exits 0, so zmdc no longer logs the stop as abnormal. An
arm that fails is logged instead of ending the server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>