[IMP] script forgejo: deviner l'adresse sans dépendre de net-tools

« hostname -I » est un drapeau de net-tools. L'inetutils d'Arch ne le connaît
pas et peut rendre le NOM de la machine — une ROOT_URL bâtie sur un nom non
résolvable est pire qu'un repli, et l'option est censée marcher sur toutes les
plateformes ERPLibre.

Trois candidats désormais, chacun validé comme adresse IPv4 avant d'être
retenu : hostname -I, puis « ip route get », puis la première adresse globale.
localhost ferme la marche — une forge joignable en local vaut mieux qu'un
script qui s'arrête. Les trois voies sont couvertes par des tests qui
bouchonnent hostname et ip.

--- EN ---

"hostname -I" is a net-tools flag. Arch's inetutils does not know it and may
return the machine NAME — a ROOT_URL built on an unresolvable name is worse than
a fallback, and this option is meant to work on every ERPLibre platform.

Three candidates now, each validated as an IPv4 address before being kept:
hostname -I, then "ip route get", then the first global address. localhost
closes the march — a forge reachable locally beats a script that stops. All
three paths are covered by tests that stub hostname and ip.

Assisted-by: Claude Opus 5
This commit is contained in:
Mathieu Benoit 2026-08-19 19:43:43 -04:00
parent 952c92b52a
commit 3a82ddbf45
2 changed files with 83 additions and 4 deletions

View file

@ -60,6 +60,30 @@ esac
say() { echo -e " $*"; }
die() { echo -e " ${Red}✗ $*${Color_Off}" >&2; exit 1; }
# Adresse par laquelle la machine est joignable, pour ROOT_URL et SSH_DOMAIN.
#
# « hostname -I » d'abord, mais PAS seulement : ce drapeau vient de net-tools et
# l'inetutils d'Arch ne le connaît pas. « ip route get » le remplace partout où
# iproute2 est là, c'est-à-dire partout. « localhost » ferme la marche : une
# forge joignable en local vaut mieux qu'un script qui s'arrête.
host_address() {
local h=""
# Chaque candidat est VALIDÉ comme adresse IPv4 avant d'être retenu : un
# « hostname » qui ne connaît pas -I peut rendre le nom de la machine, et
# une ROOT_URL bâtie sur un nom non résolvable est pire qu'un repli.
for h in \
"$(hostname -I 2>/dev/null | awk '{print $1}')" \
"$(ip -4 route get 1 2>/dev/null | awk '{print $7; exit}')" \
"$(ip -4 -o addr show scope global 2>/dev/null \
| awk '{split($4, a, "/"); print a[1]; exit}')"
do
case "$h" in
[0-9]*.[0-9]*.[0-9]*.[0-9]*) echo "$h"; return 0 ;;
esac
done
echo localhost
}
# --- 1. Architecture -------------------------------------------------------
# Forgejo publie amd64, arm64 et arm-6. PAS de s390x : sur cette architecture
# il faudrait le bâtir depuis les sources en Go, ce que ce script ne fait pas —
@ -158,8 +182,7 @@ sudo chmod 770 "$CONF_DIR"
if sudo test -f "$CONF"; then
say "configuration conservée : $CONF"
else
host=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -n "$host" ] || host=localhost
host=$(host_address)
# Les QUATRE secrets, et pas seulement les deux évidents. Vécu : sans
# « oauth2.JWT_SECRET », Forgejo tente de l'écrire dans app.ini au
# démarrage, n'y arrive pas — le fichier appartient à root — et s'arrête
@ -291,8 +314,7 @@ else
fi
# --- 9. Résumé -------------------------------------------------------------
host=$(hostname -I 2>/dev/null | awk '{print $1}')
[ -n "$host" ] || host=localhost
host=$(host_address)
version=$("$BIN" --version 2>/dev/null | head -1)
say "${Green}Forgejo prêt${Color_Off} : http://$host:$HTTP_PORT/"
say " $version"

View file

@ -204,6 +204,63 @@ class TestTheScript(unittest.TestCase):
self.assertNotIn("Version de Forgejo introuvable", out)
class TestHostAddress(unittest.TestCase):
"""L'adresse qui va dans ROOT_URL et SSH_DOMAIN, sur trois terrains.
« hostname -I » vient de net-tools : l'inetutils d'Arch ne connaît pas ce
drapeau et peut rendre le NOM de la machine. Une ROOT_URL bâtie sur un nom
non résolvable est pire qu'un repli, d'où la validation de la forme.
"""
def _host_address(self, stubs):
"""Extrait la fonction du script et l'exécute avec un PATH bouchonné."""
body = SCRIPT.read_text()
start = body.index("host_address() {")
end = body.index("\n}", start) + 2
fn = body[start:end]
with tempfile.TemporaryDirectory() as tmp:
bin_dir = pathlib.Path(tmp) / "bin"
bin_dir.mkdir()
for name, script in stubs.items():
(bin_dir / name).write_text(f"#!/bin/bash\n{script}\n")
(bin_dir / name).chmod(0o755)
res = subprocess.run(
["bash", "-c", fn + "\nhost_address"],
capture_output=True,
text=True,
env=dict(os.environ, PATH=f"{bin_dir}:/usr/bin:/bin"),
timeout=30,
)
return res.stdout.strip()
def test_it_takes_the_address_hostname_gives(self):
got = self._host_address({"hostname": "echo 10.1.2.3"})
self.assertEqual("10.1.2.3", got)
def test_a_hostname_that_returns_a_name_is_rejected(self):
"""Le cas Arch : on tombe alors sur « ip », et non sur un nom."""
got = self._host_address(
{
"hostname": "echo erplibre-arch",
"ip": "echo '1.0.0.1 via 10.0.0.1 dev eth0 src 10.9.9.9 uid 0'",
}
)
self.assertEqual("10.9.9.9", got)
def test_without_hostname_nor_ip_it_falls_back_to_localhost(self):
"""Une forge joignable en local vaut mieux qu'un script qui s'arrête."""
got = self._host_address({"hostname": "exit 1", "ip": "exit 1"})
self.assertEqual("localhost", got)
def test_it_never_returns_an_empty_string(self):
"""Une ROOT_URL « http://:3000/ » ne mène nulle part."""
for stubs in (
{"hostname": "echo", "ip": "echo"},
{"hostname": "exit 2", "ip": "exit 2"},
):
self.assertTrue(self._host_address(stubs), stubs)
class TestTheScriptGuards(unittest.TestCase):
"""Quatre pièges rencontrés en le mettant au point, tous mesurés."""