From 3d2e42d9f18a5c2451f47d512db2453fc8e020ff Mon Sep 17 00:00:00 2001 From: GnomeZworc Date: Fri, 14 Aug 2026 12:42:26 +0200 Subject: [PATCH] f-37: scripts: refacto deploy.sh Signed-off-by: GnomeZworc --- .forgejo/workflows/prerelease.yml | 29 ++- scripts/bootstrap_kvm.sh | 168 ++++++++++++++ scripts/deploy.sh | 372 ++++++++++++++++++++++++------ 3 files changed, 493 insertions(+), 76 deletions(-) create mode 100755 scripts/bootstrap_kvm.sh mode change 100644 => 100755 scripts/deploy.sh diff --git a/.forgejo/workflows/prerelease.yml b/.forgejo/workflows/prerelease.yml index 1869374..03fb7e6 100644 --- a/.forgejo/workflows/prerelease.yml +++ b/.forgejo/workflows/prerelease.yml @@ -42,27 +42,40 @@ jobs: goarch: ${{ matrix.goarch }} binari: ${{ matrix.binaries }} secrets: inherit - upload-scripts: + # Scripts et units systemd publiés comme assets de release : deploy.sh les + # installe depuis la release, plus depuis la branche. Ajouter une entrée ici + # suffit à livrer un nouveau fichier. + upload-assets: runs-on: docker needs: [set-release-target] strategy: matrix: - script: - - run-dnsmasq-in-netns.sh + include: + - path: scripts/run-dnsmasq-in-netns.sh + name: run-dnsmasq-in-netns.sh + - path: systemd/agent.service + name: agent.service + - path: systemd/dnsmasq@.service + name: dnsmasq@.service + - path: systemd/metadata@.service + name: metadata@.service steps: - uses: actions/checkout@v3 - name: Move asset run: | mkdir -p "dist" - cp scripts/${{ matrix.script }} dist/ - - name: Upload script + cp "${{ matrix.path }}" dist/ + - name: Upload asset uses: actions/upload-artifact@v3 with: - name: ${{ matrix.script }}-${{ needs.set-release-target.outputs.release_cible }} - path: dist/${{ matrix.script }} + name: ${{ matrix.name }}-${{ needs.set-release-target.outputs.release_cible }} + path: dist/${{ matrix.name }} prerelease: runs-on: docker - needs: [set-release-target, build] + # upload-assets est dans les needs : sans lui, le job release peut appeler + # download-artifact avant la fin des uploads et publier une release + # incomplète (scripts et units manquants de façon non déterministe). + needs: [set-release-target, build, upload-assets] uses: ./.forgejo/workflows/release.yml with: tag: ${{ needs.set-release-target.outputs.release_cible }} diff --git a/scripts/bootstrap_kvm.sh b/scripts/bootstrap_kvm.sh new file mode 100755 index 0000000..b6473d2 --- /dev/null +++ b/scripts/bootstrap_kvm.sh @@ -0,0 +1,168 @@ +#!/bin/bash +# +# Préparation d'un host au profil kvm : paquets, kernel, réseau de base. +# +# Ce fichier n'est pas exécutable seul : il est *sourcé* par deploy.sh quand +# --bootstrap est demandé, et réutilise ses helpers (run, info, warn, die, +# exec_with_dry_run, FLAGS_TRUE). Conséquences : +# - aucun effet de bord au chargement : uniquement des définitions, +# - aucune variable globale de deploy.sh redéfinie ici (SCRIPT_PATH, TAG, +# BIN_PATH, ASSETS…) : le sourcing les écraserait, +# - pas de `set -e` ni de garde `BASH_SOURCE` en fin de fichier. +# +# Contrat : tout bootstrap_.sh expose bootstrap_host() avec cette +# signature. deploy.sh n'a rien à savoir du contenu du profil. +# +# L'host est stateless (root sur tmpfs) : rien de ce qui suit ne survit à un +# reboot, deploy.sh --bootstrap est rejoué à chaque démarrage. + +KVM_PACKAGES="qemu-system-x86 ovmf dnsmasq ebtables iptables nfs-common jq curl" + +kvm_packages () { + DRY_RUN="${1}" + WITH_PACKAGES="${2}" + + [[ ${WITH_PACKAGES} -eq ${FLAGS_TRUE} ]] || { info "paquets : ignorés (--nopackages)"; return 0; } + + info "paquets" + run "${DRY_RUN}" "DEBIAN_FRONTEND=noninteractive apt-get update" + run "${DRY_RUN}" "DEBIAN_FRONTEND=noninteractive apt-get install -y ${KVM_PACKAGES}" + + # Le dnsmasq système prendrait le port 53 en concurrence des instances + # dnsmasq@ lancées par l'agent dans les netns. + run "${DRY_RUN}" "systemctl disable --now dnsmasq.service || true" + run "${DRY_RUN}" "systemctl mask dnsmasq.service" +} + +kvm_kernel () { + DRY_RUN="${1}" + + info "kernel" + + # modprobe AVANT sysctl : les clés net.bridge.* n'existent pas tant que + # br_netfilter n'est pas chargé, et sysctl --system échouerait. + run "${DRY_RUN}" "modprobe br_netfilter" + run "${DRY_RUN}" "echo br_netfilter > /etc/modules-load.d/two.conf" + + # bridge-nf-call-iptables est requis par le DNAT metadata (169.254.169.254, + # cf. internal/iptables) : sans lui, iptables ne voit pas le trafic bridgé + # des VMs. Contrepartie : tout le trafic inter-VM traverse les tables nat. + run "${DRY_RUN}" "printf 'net.ipv4.ip_forward = 1\nnet.bridge.bridge-nf-call-iptables = 1\n' > /etc/sysctl.d/90-two.conf" + run "${DRY_RUN}" "sysctl --system >/dev/null" +} + +# Bridge vide, up, sans STP. +kvm_ensure_bridge () { + DRY_RUN="${1}" + NAME="${2}" + + if ! ip link show dev "${NAME}" >/dev/null 2>&1 + then + run "${DRY_RUN}" "ip link add name '${NAME}' type bridge" + fi + run "${DRY_RUN}" "ip link set dev '${NAME}' type bridge stp_state 0" + run "${DRY_RUN}" "ip link set up dev '${NAME}'" +} + +kvm_network () { + DRY_RUN="${1}" + WITH_NETWORK="${2}" + UPLINK="${3}" + BRIDGE="${4}" + PUBLIC_BRIDGE="${5}" + ROLLBACK_DELAY="${6}" + + [[ ${WITH_NETWORK} -eq ${FLAGS_TRUE} ]] || { info "réseau : ignoré (--nonetwork)"; return 0; } + + info "réseau" + + # Bridge public : créé à vide, réservé pour un usage futur. Aucune adresse, + # aucun esclave, pas référencé dans la config agent. + kvm_ensure_bridge "${DRY_RUN}" "${PUBLIC_BRIDGE}" + + MASTER="" + [[ ${DRY_RUN} -ne ${FLAGS_TRUE} ]] && MASTER=$(ip -j link show dev "${UPLINK}" | jq -r '.[0].master // ""') + if [[ "${MASTER}" == "${BRIDGE}" ]] + then + info " ${UPLINK} déjà esclave de ${BRIDGE} — migration ignorée" + kvm_ensure_bridge "${DRY_RUN}" "${BRIDGE}" + return 0 + fi + + # IP, préfixe et gateway dérivés de l'uplink : rien en dur. Le filtre + # inet/global évite de tomber sur une IPv6 ou une adresse de lien. + if [[ ${DRY_RUN} -eq ${FLAGS_TRUE} ]] + then + IP=""; PREFIX=""; GW="" + else + ADDR_JSON=$(ip -j addr show dev "${UPLINK}") + IP=$(echo "${ADDR_JSON}" | jq -r '[.[0].addr_info[] | select(.family=="inet" and .scope=="global")][0].local // ""') + PREFIX=$(echo "${ADDR_JSON}" | jq -r '[.[0].addr_info[] | select(.family=="inet" and .scope=="global")][0].prefixlen // ""') + GW=$(ip -j route show default dev "${UPLINK}" | jq -r '.[0].gateway // ""') + + [[ -n "${IP}" && -n "${PREFIX}" ]] || die "aucune adresse IPv4 globale sur ${UPLINK}" + [[ -n "${GW}" ]] || die "aucune route par défaut via ${UPLINK}" + fi + + info " ${UPLINK} : ${IP}/${PREFIX} gw ${GW} → ${BRIDGE}" + + # Cette étape coupe le réseau de l'host si elle échoue à mi-parcours, sans + # console de secours. Deux garde-fous : + # - un rollback armé avant d'y toucher : rien n'étant écrit sur disque, un + # reboot ramène la configuration d'origine. Désarmé si le test passe. + # - la séquence tourne sous systemd et non dans la session SSH : une + # coupure de SSH ne l'interrompt plus à mi-chemin. + run "${DRY_RUN}" "systemctl stop two-net-rollback.timer 2>/dev/null || true" + run "${DRY_RUN}" "systemd-run --collect --unit=two-net-rollback --on-active=${ROLLBACK_DELAY} systemctl reboot" + + MIGRATE=$(cat </dev/null 2>&1 || ip link add name '${BRIDGE}' type bridge +ip link set dev '${BRIDGE}' type bridge stp_state 0 +ip link set up dev '${BRIDGE}' +ip link set '${UPLINK}' master '${BRIDGE}' +ip addr add '${IP}/${PREFIX}' dev '${BRIDGE}' +ip route replace default via '${GW}' dev '${BRIDGE}' +ip addr del '${IP}/${PREFIX}' dev '${UPLINK}' +pkill dhclient || true +EOF +) + + if [[ ${DRY_RUN} -eq ${FLAGS_TRUE} ]] + then + echo "# systemd-run --wait --collect --service-type=oneshot --unit=two-net-migrate /bin/bash <<'EOF'" + echo "${MIGRATE}" | sed -e 's/^/# /' + echo "# EOF" + else + MIGRATE_SCRIPT=$(mktemp /run/two-net-migrate.XXXXXX) + printf '%s\n' "${MIGRATE}" > "${MIGRATE_SCRIPT}" + systemctl reset-failed two-net-migrate.service 2>/dev/null || true + systemd-run --wait --collect --service-type=oneshot --unit=two-net-migrate \ + /bin/bash "${MIGRATE_SCRIPT}" \ + || die "migration réseau échouée — rollback armé dans ${ROLLBACK_DELAY}s (reboot)" + rm -f "${MIGRATE_SCRIPT}" + + # Vérification de connectivité avant de désarmer : seul critère qui + # distingue un succès d'un host qu'on vient d'isoler. + ping -c 2 -W 2 "${GW}" >/dev/null 2>&1 \ + || die "gateway ${GW} injoignable après migration — rollback armé dans ${ROLLBACK_DELAY}s (reboot)" + fi + + run "${DRY_RUN}" "systemctl stop two-net-rollback.timer 2>/dev/null || true" + info " rollback désarmé" +} + +# Point d'entrée appelé par deploy.sh --bootstrap. +bootstrap_host () { + DRY_RUN="${1}" + WITH_PACKAGES="${2}" + WITH_NETWORK="${3}" + UPLINK="${4}" + BRIDGE="${5}" + PUBLIC_BRIDGE="${6}" + ROLLBACK_DELAY="${7}" + + kvm_packages "${DRY_RUN}" "${WITH_PACKAGES}" + kvm_kernel "${DRY_RUN}" + kvm_network "${DRY_RUN}" "${WITH_NETWORK}" "${UPLINK}" "${BRIDGE}" "${PUBLIC_BRIDGE}" "${ROLLBACK_DELAY}" +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh old mode 100644 new mode 100755 index 7256505..780dd00 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -10,6 +10,13 @@ case "${unameOut}" in esac SCRIPT_PATH="scripts/deploy.sh" +UNIT_DIR="/etc/systemd/system" +# Copies locales des scripts de bootstrap, déposées à la main sur l'host. +SCRIPTS_DIR="/opt/two/scripts" + +info () { echo "== ${1}"; } +warn () { echo "!! ${1}" >&2; } +die () { echo "!! ${1}" >&2; exit 1; } exec_with_dry_run () { if [[ ${1} -eq ${FLAGS_TRUE} ]]; then @@ -26,6 +33,14 @@ exec_with_dry_run () { return 0 } +# `set -e` est inopérant dans main : bash désactive errexit dans le corps d'une +# fonction appelée depuis une liste `||` (cf. dernières lignes du fichier), y +# compris dans les fonctions qu'elle appelle. Les étapes qui ne doivent pas +# échouer silencieusement passent donc par run(). +run () { + exec_with_dry_run "${1}" "${2}" || die "échec : ${2}" +} + check_latest_script () { REMOTE_URL="${1}" LOCAL_PATH="${2}" @@ -45,107 +60,328 @@ list_active_units () { | awk '{print $1}' || true } +# Units non instanciables du profil (agent.service) : celles qu'on arrête et +# démarre nommément. +profile_main_units () { + for unit in $(profile_units "${1}") + do + case "${unit}" in + *@.service) continue ;; + *) echo "${unit}" ;; + esac + done +} + +# Instances actives des templates du profil (dnsmasq@vpc_br, metadata@i-xxx). +# À relever AVANT l'arrêt : c'est la seule façon de savoir lesquelles +# redémarrer ensuite. +active_instances () { + for unit in $(profile_units "${1}") + do + case "${unit}" in + *@.service) list_active_units "${unit%@.service}@*" ;; + esac + done +} + stop_services () { DRY_RUN="${1}" - METADATA_UNITS="${2}" - DNSMASQ_UNITS="${3}" + PROFILE="${2}" + INSTANCES="${3}" - # L'agent en premier : il pilote les autres units, on ne veut pas qu'il - # observe leur disparition. - exec_with_dry_run "${DRY_RUN}" "systemctl stop agent.service" - - for unit in ${METADATA_UNITS} ${DNSMASQ_UNITS} + # Les units principales en premier : sur le profil kvm, l'agent pilote les + # autres units, on ne veut pas qu'il observe leur disparition. + for unit in $(profile_main_units "${PROFILE}") do - exec_with_dry_run "${DRY_RUN}" "systemctl stop '${unit}'" + run "${DRY_RUN}" "systemctl stop '${unit}'" + done + + for unit in ${INSTANCES} + do + run "${DRY_RUN}" "systemctl stop '${unit}'" done } start_services () { DRY_RUN="${1}" - METADATA_UNITS="${2}" - DNSMASQ_UNITS="${3}" + PROFILE="${2}" + INSTANCES="${3}" - # Ordre inverse de l'arrêt : les dépendances d'abord, l'agent en dernier. - for unit in ${DNSMASQ_UNITS} ${METADATA_UNITS} + # Ordre inverse de l'arrêt : les dépendances d'abord, les units principales + # en dernier. Une instance qui refuse de repartir (VM disparue entre-temps) + # ne doit pas empêcher le redémarrage de l'agent, d'où le simple warn. + for unit in ${INSTANCES} do - exec_with_dry_run "${DRY_RUN}" "systemctl start '${unit}'" + exec_with_dry_run "${DRY_RUN}" "systemctl start '${unit}'" \ + || warn "démarrage de ${unit} en échec — poursuite" done - exec_with_dry_run "${DRY_RUN}" "systemctl start agent.service" + for unit in $(profile_main_units "${PROFILE}") + do + run "${DRY_RUN}" "systemctl start '${unit}'" + done } -download_binaries () { +# ------------------------------------------------------------------- profils +# +# Un profil décrit ce qu'un host doit recevoir : ses units systemd, ses +# exécutables et son script de préparation. Rien n'est déployé qui ne soit +# listé par le profil — un host intel ne reçoit pas l'agent kvm. Ajouter un +# profil = ajouter une branche dans les trois fonctions ci-dessous et livrer un +# bootstrap_.sh exposant bootstrap_host(). +# +# kvm hyperviseur QEMU/KVM : agent + metadata + dnsmasq, bridges, br_netfilter +# intel réservé, non implémenté + +profile_units () { + case "${1}" in + kvm) echo "agent.service dnsmasq@.service metadata@.service" ;; + intel) echo "" ;; + *) return 1 ;; + esac +} + +# Noms courts des exécutables, tels qu'ils apparaîtront dans /opt/two/bin. +# Un asset de release les porte soit à l'identique (run-dnsmasq-in-netns.sh), +# soit suffixé par la plateforme (agent → agent_linux_amd64). +profile_binaries () { + case "${1}" in + kvm) echo "agent metadata run-dnsmasq-in-netns.sh" ;; + intel) echo "" ;; + *) return 1 ;; + esac +} + +# Script de préparation de l'host, sourcé par run_bootstrap. Vide = le profil +# n'a rien à préparer. Paquets, kernel et réseau appartiennent au profil, pas à +# deploy.sh. +profile_bootstrap () { + case "${1}" in + kvm) echo "bootstrap_kvm.sh" ;; + intel) echo "" ;; + *) return 1 ;; + esac +} + +# Charge le script de bootstrap du profil et lui passe la main. +# +# Même logique que ./libs/shflags : copie locale d'abord, dépôt ensuite. +# 1. /opt/two/scripts/ — déposé à la main sur l'host, permet de figer ou +# de patcher un profil sans toucher au dépôt +# 2. la branche demandée — cas courant sur un host neuf +# +# Le fichier ne contenant que des définitions de fonctions, il supporte l'`eval` +# — au contraire de deploy.sh, dont la garde BASH_SOURCE finale déclencherait +# main(). `curl --fail` reste indispensable : sans lui, le corps d'un 404 +# (« Not found. ») serait évalué comme du shell. En cas d'échec, curl ne produit +# rien, l'eval est un no-op et c'est le contrôle de bootstrap_host() qui tranche. +run_bootstrap () { DRY_RUN="${1}" - TAG="${2}" + PROFILE="${2}" GIT_SERVER="${3}" REPO_PATH="${4}" + BRANCH="${5}" - #'.[0].assets.[].browser_download_url' - [[ "${TAG}" == "" ]] && TAG=$(curl --silent "${GIT_SERVER}api/v1/repos/${REPO_PATH}releases/?limit=1" | jq -r '.[0].tag_name') - echo "Deploy ${TAG} binaries" + NAME=$(profile_bootstrap "${PROFILE}") || die "profil inconnu : ${PROFILE}" + [[ -n "${NAME}" ]] || { info "bootstrap : rien à préparer pour le profil ${PROFILE}"; return 0; } - BIN_PATH="/opt/two/${TAG}/bin/" - LN_PATH="/opt/two/bin/" + BOOTSTRAP_URL="${GIT_SERVER}${REPO_PATH}raw/branch/${BRANCH}scripts/${NAME}" + info "bootstrap du profil ${PROFILE} (${SCRIPTS_DIR}/${NAME} ou ${BOOTSTRAP_URL})" - exec_with_dry_run "${DRY_RUN}" "mkdir -p \"${BIN_PATH}\"" - exec_with_dry_run "${DRY_RUN}" "mkdir -p \"${LN_PATH}\"" + # shellcheck source=/dev/null + [[ -f "${SCRIPTS_DIR}/${NAME}" ]] && . "${SCRIPTS_DIR}/${NAME}" || eval "$(curl --silent --fail "${BOOTSTRAP_URL}")" - ASSETS=$(curl --silent "${GIT_SERVER}api/v1/repos/${REPO_PATH}releases/tags/${TAG}" | jq -c '.assets[]') + 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" - # Téléchargement d'abord, dans le répertoire versionné : aucun impact sur - # les services en cours, donc aucune raison de les arrêter pendant ce temps. - while read -r tmp - do - [[ -z "${tmp}" ]] && continue - BINARY_NAME=$(echo "${tmp}" | jq -r '.name') - BINARY_URL=$(echo "${tmp}" | jq -r '.browser_download_url') - exec_with_dry_run "${DRY_RUN}" "curl --silent '${BINARY_URL}' -o '${BIN_PATH}${BINARY_NAME}'" - exec_with_dry_run "${DRY_RUN}" "chmod +x '${BIN_PATH}${BINARY_NAME}'" - done <<< "${ASSETS}" - - # Les units actives sont relevées avant l'arrêt : c'est la seule façon de - # savoir lesquelles redémarrer ensuite (instances dnsmasq@ et metadata@). - METADATA_UNITS=$(list_active_units 'metadata@*') - DNSMASQ_UNITS=$(list_active_units 'dnsmasq@*') - - stop_services "${DRY_RUN}" "${METADATA_UNITS}" "${DNSMASQ_UNITS}" - - # Bascule des liens symboliques, services arrêtés : c'est la seule fenêtre - # d'indisponibilité réelle. - while read -r tmp - do - [[ -z "${tmp}" ]] && continue - BINARY_NAME=$(echo "${tmp}" | jq -r '.name') - BINARY_SHORT_NAME=$(echo "${BINARY_NAME}" | cut -d_ -f 1) - exec_with_dry_run "${DRY_RUN}" "rm -f '${LN_PATH}${BINARY_SHORT_NAME}'" - exec_with_dry_run "${DRY_RUN}" "ln -s '${BIN_PATH}${BINARY_NAME}' '${LN_PATH}${BINARY_SHORT_NAME}'" - done <<< "${ASSETS}" - - start_services "${DRY_RUN}" "${METADATA_UNITS}" "${DNSMASQ_UNITS}" + bootstrap_host "${DRY_RUN}" "${FLAGS_packages}" "${FLAGS_network}" \ + "${FLAGS_uplink}" "${FLAGS_bridge}" "${FLAGS_pub_bridge}" "${FLAGS_rollback}" } +# ------------------------------------------------------------------- release + +resolve_tag () { + TAG="${1}" + GIT_SERVER="${2}" + 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' +} + +# Assets de la release, un objet JSON compact par ligne. +release_assets () { + GIT_SERVER="${1}" + REPO_PATH="${2}" + RELEASE_TAG="${3}" + + curl --silent "${GIT_SERVER}api/v1/repos/${REPO_PATH}releases/tags/${RELEASE_TAG}" | jq -c '.assets[]' +} + +# Nom complet de l'asset correspondant à un nom court : exact, ou suffixé par la +# plateforme (agent → agent_linux_amd64). Vide si absent de la release. +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 +} + +# Télécharge dans les répertoires versionnés les seuls assets réclamés par le +# profil. Aucun impact sur les services en cours, donc aucune raison de les +# arrêter pendant ce temps. +fetch_assets () { + DRY_RUN="${1}" + PROFILE="${2}" + ASSETS="${3}" + + 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 + UNIT_URL=$(asset_url "${ASSETS}" "${unit}") + [[ -n "${UNIT_URL}" ]] || die "unit absente de la release ${TAG} : ${unit}" + run "${DRY_RUN}" "curl --silent '${UNIT_URL}' -o '${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}" + BIN_URL=$(asset_url "${ASSETS}" "${FULL_NAME}") + run "${DRY_RUN}" "curl --silent '${BIN_URL}' -o '${BIN_PATH}${FULL_NAME}'" + run "${DRY_RUN}" "chmod +x '${BIN_PATH}${FULL_NAME}'" + done +} + +# Installe les units du profil depuis la release. Les templates (dnsmasq@, +# metadata@) ne sont pas activés : l'agent les instancie au fil des subnets et +# des VMs. +install_units () { + DRY_RUN="${1}" + PROFILE="${2}" + + 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 +} + +# Bascule des liens symboliques, services arrêtés : c'est la seule fenêtre +# d'indisponibilité réelle. +switch_binaries () { + DRY_RUN="${1}" + PROFILE="${2}" + ASSETS="${3}" + + 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_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' FLAGS "$@" || exit $? eval set -- "${FLAGS_ARGV}" - SCRIPT_URL="${FLAGS_git_server}${FLAGS_repo_path}raw/branch/${FLAGS_branch}${SCRIPT_PATH}" - check_latest_script "${SCRIPT_URL}" "${0}" || ( - [[ ${FLAGS_up_script} -eq ${FLAGS_TRUE} ]] && \ - exec_with_dry_run "${FLAGS_dryrun}" "curl --silent '${SCRIPT_URL}' -o '${0}'" - exit 1 - ) + profile_units "${FLAGS_profile}" >/dev/null || die "profil inconnu : ${FLAGS_profile}" - download_binaries "${FLAGS_dryrun}" "${FLAGS_tag}" "${FLAGS_git_server}" "${FLAGS_repo_path}" + # --noup_script court-circuite la vérification : seul moyen de dérouler un + # script en cours de modification, encore absent de la branche. + # + # L'ancien idiome `check_latest_script || ( ... ; exit 1 )` ne s'arrêtait pas + # réellement : le `exit 1` d'un sous-shell placé en fin de liste `||` est + # ignoré parce qu'errexit est désactivé dans le corps de main. Le script + # s'écrasait puis continuait à exécuter sa version périmée en mémoire. D'où + # le die() explicite. + if [[ ${FLAGS_up_script} -eq ${FLAGS_TRUE} ]] + then + 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 + + # Préparation de l'host avant tout téléchargement : elle ne dépend pas de la + # release, et les paquets comme le réseau conditionnent la suite. + [[ ${FLAGS_bootstrap} -eq ${FLAGS_TRUE} ]] && \ + run_bootstrap "${FLAGS_dryrun}" "${FLAGS_profile}" "${FLAGS_git_server}" "${FLAGS_repo_path}" "${FLAGS_branch}" + + 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}" + + fetch_assets "${FLAGS_dryrun}" "${FLAGS_profile}" "${ASSETS}" + + # Les units sont installées avant la bascule : stop_services arrête les + # units principales du profil, qui doivent donc déjà exister. + 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) \ No newline at end of file +[[ "${BASH_SOURCE[0]}" == "" ]] && (main "$@" || exit 1)