two/scripts/deploy.sh
GnomeZworc 9b90defbdb
f-46: dhcp: add the dhcp binary, its unit and its wrapper #46
cmd/dhcp reçoit quatre paramètres en clair — interface, state, socket, conf —
sur le modèle de dnsmasq. Il ne compose aucun chemin, ne découpe aucun nom
composite et ignore le netns dans lequel il tourne : seul le wrapper en a besoin,
pour y entrer.

La boucle de lecture UDP est écrite à la main plutôt que confiée à
server4.Serve, qui lance une goroutine par datagramme sans borne et ne pose
aucun recover. Elle traite en ligne, réutilise un tampon de 1500 octets et pose
un recover par datagramme. NewIPv4UDPConn est conservé pour le SO_BROADCAST et
le bind à l'interface. Une réponse destinée à un client sans adresse part en
broadcast.

dhcp.run_dir n'est pas une clé de configuration : le wrapper code /run/two/dhcp
en dur et le Go utilise dhcpapi.DefaultRunDir, comme dhcp.DefaultConfDir pour
dnsmasq. Un test lit le script et vérifie que les deux s'accordent.

Le défaut dhcp.backend reste dnsmasq : un agent.yml de 0.1.0 se comporte comme
avant. deploy.sh et le pipeline publient le binaire, l'unit et le script.

La boucle est testée sur une vraie socket UDP en loopback. Neuf mutations, dont
une qui a révélé que le recover n'était couvert par rien.

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-09-05 23:15:48 +02:00

391 lines
12 KiB
Bash
Executable file

#!/bin/bash
set -e
SED_PARAM=""
unameOut="$(uname -s)"
case "${unameOut}" in
Linux*) SED_PARAM=" -i ";;
Darwin*) SED_PARAM=" -i '' ";;
*) exit 1
esac
SCRIPT_PATH="scripts/deploy.sh"
UNIT_DIR="/etc/systemd/system"
SCRIPTS_DIR="/opt/two/scripts"
# Asset listant les sommes de contrôle des autres, au format sha256sum.
MANIFEST_NAME="SHA256SUMS"
info () { echo "== ${1}"; }
warn () { echo "!! ${1}" >&2; }
die () { echo "!! ${1}" >&2; exit 1; }
exec_with_dry_run () {
if [[ ${1} -eq ${FLAGS_TRUE} ]]; then
echo "# ${2}"
else
eval "${2}" 2> /tmp/error || \
{
echo -e "failed with following error";
local output; output=$(cat /tmp/error | sed -e "s/^/ error -> /g");
echo -e "${output}";
return 1;
}
fi
return 0
}
run () {
exec_with_dry_run "${1}" "${2}" || die "échec : ${2}"
}
check_latest_script () {
local REMOTE_URL="${1}"
local LOCAL_PATH="${2}"
local REMOTE_SUM LOCAL_SUM
REMOTE_SUM=$(curl --silent "${REMOTE_URL}" | sha256sum)
LOCAL_SUM=$(cat ${LOCAL_PATH} | sha256sum)
[[ "${REMOTE_SUM}" == "${LOCAL_SUM}" ]] || return 1
return 0
}
list_active_units () {
local PATTERN="${1}"
systemctl list-units --state=active --no-legend --plain "${PATTERN}" 2>/dev/null \
| awk '{print $1}' || true
}
# Units non instanciables du profil (agent.service) : celles qu'on arrête et
# démarre nommément.
profile_main_units () {
local unit
for unit in $(profile_units "${1}")
do
case "${unit}" in
*@.service) continue ;;
*) echo "${unit}" ;;
esac
done
}
active_instances () {
local unit
for unit in $(profile_units "${1}")
do
case "${unit}" in
*@.service) list_active_units "${unit%@.service}@*" ;;
esac
done
}
stop_services () {
local DRY_RUN="${1}"
local PROFILE="${2}"
local INSTANCES="${3}"
local unit
for unit in $(profile_main_units "${PROFILE}")
do
run "${DRY_RUN}" "systemctl stop '${unit}'"
done
for unit in ${INSTANCES}
do
run "${DRY_RUN}" "systemctl stop '${unit}'"
done
}
start_services () {
local DRY_RUN="${1}"
local PROFILE="${2}"
local INSTANCES="${3}"
local unit
for unit in ${INSTANCES}
do
exec_with_dry_run "${DRY_RUN}" "systemctl start '${unit}'" \
|| warn "démarrage de ${unit} en échec — poursuite"
done
for unit in $(profile_main_units "${PROFILE}")
do
run "${DRY_RUN}" "systemctl start '${unit}'"
done
}
profile_units () {
case "${1}" in
kvm) echo "agent.service dnsmasq@.service dhcp@.service metadata@.service" ;;
intel) echo "" ;;
*) return 1 ;;
esac
}
profile_binaries () {
case "${1}" in
kvm) echo "agent metadata dhcp run-dnsmasq-in-netns.sh run-dhcp-in-netns.sh" ;;
intel) echo "" ;;
*) return 1 ;;
esac
}
profile_bootstrap () {
case "${1}" in
kvm) echo "bootstrap_kvm.sh" ;;
intel) echo "" ;;
*) return 1 ;;
esac
}
run_bootstrap () {
local DRY_RUN="${1}"
local PROFILE="${2}"
local GIT_SERVER="${3}"
local REPO_PATH="${4}"
local BRANCH="${5}"
local NAME BOOTSTRAP_URL
NAME=$(profile_bootstrap "${PROFILE}") || die "profil inconnu : ${PROFILE}"
[[ -n "${NAME}" ]] || { info "bootstrap : rien à préparer pour le profil ${PROFILE}"; return 0; }
BOOTSTRAP_URL="${GIT_SERVER}${REPO_PATH}raw/branch/${BRANCH}scripts/${NAME}"
info "bootstrap du profil ${PROFILE} (${SCRIPTS_DIR}/${NAME} ou ${BOOTSTRAP_URL})"
# shellcheck source=/dev/null
[[ -f "${SCRIPTS_DIR}/${NAME}" ]] && . "${SCRIPTS_DIR}/${NAME}" || eval "$(curl --silent --fail "${BOOTSTRAP_URL}")"
command -v bootstrap_host >/dev/null 2>&1 \
|| die "bootstrap_host() introuvable — ni ${SCRIPTS_DIR}/${NAME} ni ${BOOTSTRAP_URL} n'ont pu être chargés"
bootstrap_host "${DRY_RUN}" "${FLAGS_packages}" "${FLAGS_network}" \
"${FLAGS_uplink}" "${FLAGS_bridge}" "${FLAGS_pub_bridge}" "${FLAGS_rollback}"
}
resolve_tag () {
local TAG="${1}"
local GIT_SERVER="${2}"
local REPO_PATH="${3}"
[[ -n "${TAG}" ]] && { echo "${TAG}"; return 0; }
curl --silent "${GIT_SERVER}api/v1/repos/${REPO_PATH}releases/?limit=1" | jq -r '.[0].tag_name'
}
release_assets () {
local GIT_SERVER="${1}"
local REPO_PATH="${2}"
local RELEASE_TAG="${3}"
curl --silent "${GIT_SERVER}api/v1/repos/${REPO_PATH}releases/tags/${RELEASE_TAG}" | jq -c '.assets[]'
}
asset_name () {
echo "${1}" | jq -r --arg s "${2}" 'select(.name == $s or (.name | startswith($s + "_"))) | .name' | head -1
}
asset_url () {
echo "${1}" | jq -r --arg n "${2}" 'select(.name == $n) | .browser_download_url' | head -1
}
release_manifest () {
local ASSETS="${1}"
local URL
URL=$(asset_url "${ASSETS}" "${MANIFEST_NAME}")
[[ -n "${URL}" ]] || return 1
curl --silent --fail "${URL}" || return 1
}
manifest_sum () {
awk -v n="${2}" '$2 == n { print $1 }' <<< "${1}" | head -1
}
sum_matches () {
local MANIFEST="${1}"
local NAME="${2}"
local FILE="${3}"
local EXPECTED ACTUAL
EXPECTED=$(manifest_sum "${MANIFEST}" "${NAME}")
[[ -n "${EXPECTED}" ]] || die "asset absent du manifeste ${MANIFEST_NAME} : ${NAME}"
[[ -f "${FILE}" ]] || return 1
ACTUAL=$(sha256sum < "${FILE}" | cut -d' ' -f1)
[[ "${ACTUAL}" == "${EXPECTED}" ]]
}
fetch_asset () {
local DRY_RUN="${1}"
local MANIFEST="${2}"
local ASSETS="${3}"
local NAME="${4}"
local DEST="${5}"
local URL
if [[ -n "${MANIFEST}" ]] && sum_matches "${MANIFEST}" "${NAME}" "${DEST}"
then
info " ${NAME} déjà présent et conforme — téléchargement évité"
return 0
fi
URL=$(asset_url "${ASSETS}" "${NAME}")
[[ -n "${URL}" ]] || die "asset absent de la release ${TAG} : ${NAME}"
run "${DRY_RUN}" "curl --silent --fail '${URL}' -o '${DEST}'"
[[ ${DRY_RUN} -eq ${FLAGS_TRUE} ]] && return 0
[[ -n "${MANIFEST}" ]] || return 0
sum_matches "${MANIFEST}" "${NAME}" "${DEST}" \
|| die "somme de contrôle incorrecte après téléchargement : ${NAME}"
}
fetch_assets () {
local DRY_RUN="${1}"
local PROFILE="${2}"
local ASSETS="${3}"
local MANIFEST="${4}"
local unit short FULL_NAME
info "assets de la release ${TAG} pour le profil ${PROFILE}"
run "${DRY_RUN}" "mkdir -p '${BIN_PATH}'"
run "${DRY_RUN}" "mkdir -p '${UNIT_PATH}'"
run "${DRY_RUN}" "mkdir -p '${LN_PATH}'"
for unit in $(profile_units "${PROFILE}")
do
fetch_asset "${DRY_RUN}" "${MANIFEST}" "${ASSETS}" "${unit}" "${UNIT_PATH}${unit}"
done
for short in $(profile_binaries "${PROFILE}")
do
FULL_NAME=$(asset_name "${ASSETS}" "${short}")
[[ -n "${FULL_NAME}" ]] || die "exécutable absent de la release ${TAG} : ${short}"
fetch_asset "${DRY_RUN}" "${MANIFEST}" "${ASSETS}" "${FULL_NAME}" "${BIN_PATH}${FULL_NAME}"
run "${DRY_RUN}" "chmod +x '${BIN_PATH}${FULL_NAME}'"
done
}
install_units () {
local DRY_RUN="${1}"
local PROFILE="${2}"
local UNITS unit
UNITS=$(profile_units "${PROFILE}") || die "profil inconnu : ${PROFILE}"
[[ -n "${UNITS}" ]] || { info "units : aucune pour le profil ${PROFILE}"; return 0; }
info "units du profil ${PROFILE}"
for unit in ${UNITS}
do
if [[ ${DRY_RUN} -ne ${FLAGS_TRUE} ]] && [[ ! -f "${UNIT_PATH}${unit}" ]]
then
die "unit absente des assets de la release ${TAG} : ${unit}"
fi
run "${DRY_RUN}" "install -m 0644 '${UNIT_PATH}${unit}' '${UNIT_DIR}/${unit}'"
done
run "${DRY_RUN}" "systemctl daemon-reload"
for unit in ${UNITS}
do
case "${unit}" in
*@.service) continue ;;
esac
run "${DRY_RUN}" "systemctl enable '${unit}'"
done
}
switch_binaries () {
local DRY_RUN="${1}"
local PROFILE="${2}"
local ASSETS="${3}"
local BINARIES INSTANCES short FULL_NAME
BINARIES=$(profile_binaries "${PROFILE}")
[[ -n "${BINARIES}" ]] || { info "bascule : aucun exécutable pour le profil ${PROFILE}"; return 0; }
info "bascule des binaires"
INSTANCES=$(active_instances "${PROFILE}")
stop_services "${DRY_RUN}" "${PROFILE}" "${INSTANCES}"
for short in ${BINARIES}
do
FULL_NAME=$(asset_name "${ASSETS}" "${short}")
run "${DRY_RUN}" "rm -f '${LN_PATH}${short}'"
run "${DRY_RUN}" "ln -s '${BIN_PATH}${FULL_NAME}' '${LN_PATH}${short}'"
done
start_services "${DRY_RUN}" "${PROFILE}" "${INSTANCES}"
}
main () {
[[ -f ./libs/shflags ]] && . ./libs/shflags || eval "$(curl --silent https://git.g3e.fr/H6N/tools/raw/branch/main/libs/shflags)"
DEFINE_boolean 'dryrun' false 'Enable dry-run mode' 'd'
DEFINE_boolean 'up_script' true 'Upgrade script' 's'
DEFINE_string 'git_server' 'https://git.g3e.fr/' 'Git Server' 'g'
DEFINE_string 'repo_path' 'syonad/two/' 'Path of repository' 'r'
DEFINE_string 'branch' 'main/' 'Branch name' 'b'
DEFINE_string 'tag' '' 'Tag name' 't'
DEFINE_string 'profile' 'kvm' 'Host profile: kvm' 'p'
DEFINE_boolean 'bootstrap' false 'Prepare the host (packages, kernel, network)' 'i'
DEFINE_boolean 'packages' true 'Install profile packages during --bootstrap' 'k'
DEFINE_boolean 'network' true 'Configure bridges during --bootstrap' 'n'
DEFINE_string 'uplink' 'eno1' 'Physical uplink interface' 'u'
DEFINE_string 'bridge' 'br-000000' 'Main bridge, uplink is enslaved to it' 'B'
DEFINE_string 'pub_bridge' 'br-public' 'Reserved empty bridge' 'P'
DEFINE_integer 'rollback' 120 'Rollback reboot delay in seconds' 'R'
DEFINE_boolean 'verify' true 'Check assets against SHA256SUMS' 'V'
FLAGS "$@" || exit $?
eval set -- "${FLAGS_ARGV}"
profile_units "${FLAGS_profile}" >/dev/null || die "profil inconnu : ${FLAGS_profile}"
if [[ ${FLAGS_up_script} -eq ${FLAGS_TRUE} ]]
then
local SCRIPT_URL="${FLAGS_git_server}${FLAGS_repo_path}raw/branch/${FLAGS_branch}${SCRIPT_PATH}"
if ! check_latest_script "${SCRIPT_URL}" "${0}"
then
run "${FLAGS_dryrun}" "curl --silent '${SCRIPT_URL}' -o '${0}'"
die "script local différent de la branche ${FLAGS_branch} — mis à jour, relancer"
fi
fi
[[ ${FLAGS_bootstrap} -eq ${FLAGS_TRUE} ]] && \
run_bootstrap "${FLAGS_dryrun}" "${FLAGS_profile}" "${FLAGS_git_server}" "${FLAGS_repo_path}" "${FLAGS_branch}"
local TAG BIN_PATH UNIT_PATH LN_PATH ASSETS MANIFEST
TAG=$(resolve_tag "${FLAGS_tag}" "${FLAGS_git_server}" "${FLAGS_repo_path}")
[[ -n "${TAG}" && "${TAG}" != "null" ]] || die "impossible de déterminer la release à déployer"
BIN_PATH="/opt/two/${TAG}/bin/"
UNIT_PATH="/opt/two/${TAG}/units/"
LN_PATH="/opt/two/bin/"
ASSETS=$(release_assets "${FLAGS_git_server}" "${FLAGS_repo_path}" "${TAG}")
[[ -n "${ASSETS}" ]] || die "aucun asset dans la release ${TAG}"
# Manifeste absent = release antérieure à sa mise en place, ou CI incomplète.
# C'est bloquant : une vérification qui se désactive d'elle-même ne vérifie
# rien. --noverify est la sortie explicite.
MANIFEST=""
if [[ ${FLAGS_verify} -eq ${FLAGS_TRUE} ]]
then
MANIFEST=$(release_manifest "${ASSETS}") \
|| die "${MANIFEST_NAME} absent de la release ${TAG} — --noverify pour déployer sans vérification"
else
warn "vérification des sommes de contrôle désactivée (--noverify)"
fi
fetch_assets "${FLAGS_dryrun}" "${FLAGS_profile}" "${ASSETS}" "${MANIFEST}"
install_units "${FLAGS_dryrun}" "${FLAGS_profile}"
switch_binaries "${FLAGS_dryrun}" "${FLAGS_profile}" "${ASSETS}"
}
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && (main "$@" || exit 1)
[[ "${BASH_SOURCE[0]}" == "" ]] && (main "$@" || exit 1)