Files
monky-deployd/packaging/install.sh
T
mdella c966450d8e fix(install): fetch from scm.tikali.ai (public project) — Gitea name is split-horizon inside the estate
Inside the estate gitea.cbs.tikali.net resolves to jump1's RED EIP (10.10.0.175),
which has no HTTP ingress, so backend boxes could not download the install
artefacts from the Gitea mirror (cbs/iac#102). scm.tikali.ai is reachable from
those boxes and the project is now public, so the GitLab generic package
registry becomes the PRIMARY source:

- packaging/install.sh: default source = scm.tikali.ai generic package registry
  (projects/69/packages/generic/monky-deployd/<ver>/...); `--source gitea` /
  MONKY_DEPLOYD_SOURCE=gitea keeps the Gitea release as the off-estate
  alternative; --base-url / MONKY_DEPLOYD_BASE_URL still override the base.
- ansible role defaults: monky_deployd_base_url/_deb_url point at the registry,
  Gitea layout kept as a commented alternative.
- README / docs/OPERATIONS.md / CLAUDE.md / CI comments + release description:
  both locations keep being published (release + release:gitea).
- Version 0.1.1 (the tag gate refuses v* tags whose version != __version__);
  tests compare against __version__ instead of a literal. No agent change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KLB7jieMNRkTsJ2epr4Ds1
2026-09-05 17:57:27 +00:00

238 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
# monky-deployd installer — Ubuntu 26.04 (verified target).
#
# curl -fsSL https://scm.tikali.ai/tikali/applications/monky/monky-deployd/-/raw/main/packaging/install.sh \
# | sudo bash -s -- --env env-qa-02 --site cbs [--transport sdk|proxy|system] [--version 0.1.1] \
# [--enrol-jwt /path/monky-host.env-qa-02.jwt] [--laptop] [--source gitlab|gitea] < bootstrap.jwt
#
# stdin (or --bootstrap-file): the ONE-TIME bootstrap deploy grant from the install kit
# (GET /v1/backends/{id}/agent/install). The enrolment JWT is read from --enrol-jwt or
# /etc/monky-deployd/enrol.jwt and is only needed when the host identity is not enrolled yet.
#
# What it does (idempotent):
# 1. apt: ziti-edge-tunnel (OpenZiti `jammy` suite) if absent, docker-compose-plugin, acl
# 2. downloads the pinned monky-deployd_<ver>_amd64.deb + .sha256 from the scm.tikali.ai package registry
# (--source gitea: the Gitea release, off-estate), verifies, installs
# 3. enrols /opt/openziti/etc/identities/monky-host.<env>.json if absent (ziti-edge-tunnel enroll),
# chown ziti:ziti 0600, switches ziti-edge-tunnel.service to `run-host` via a drop-in
# 4. writes /etc/monky-deployd/config.yaml, ACLs so user monky-deployd can read the identity,
# the bootstrap grant 0600, (proxy transport: proxy.env + monky-deployd-proxy.service)
# 5. enables monky-deployd.timer, runs one tick, deletes the enrol JWT, prints the checklist
set -euo pipefail
umask 077
DEFAULT_VERSION="0.1.1"
# Download source. PRIMARY is the public GitLab project's generic package registry on scm.tikali.ai:
# inside the estate gitea.cbs.tikali.net is split-horizon to jump1's RED EIP (10.10.0.175), which has
# no HTTP ingress, so backend boxes cannot reach the Gitea mirror (cbs/iac#102); scm.tikali.ai they can.
# `--source gitea` keeps the Gitea release as the off-estate alternative. --base-url / MONKY_DEPLOYD_BASE_URL
# override the base for whichever layout is selected.
GITLAB_BASE_URL="https://scm.tikali.ai/api/v4/projects/69/packages/generic/monky-deployd"
GITEA_BASE_URL="https://gitea.cbs.tikali.net/mdella/monky-deployd"
SOURCE="${MONKY_DEPLOYD_SOURCE:-gitlab}"
BASE_URL="${MONKY_DEPLOYD_BASE_URL:-}"
OPENZITI_SUITE="${OPENZITI_SUITE:-jammy}"
IDENTITY_DIR="/opt/openziti/etc/identities"
ETC="/etc/monky-deployd"
ENV_ID="" SITE="" TRANSPORT="sdk" VERSION="$DEFAULT_VERSION" ENROL_JWT="" BOOTSTRAP_FILE="" NO_RUN="" FORCE_CONFIG="" BAO_CA="" LAPTOP="false"
usage() { sed -n '2,20p' "$0"; exit "${1:-0}"; }
die() { echo "install.sh: $*" >&2; exit 1; }
log() { echo "==> $*"; }
while [ $# -gt 0 ]; do
case "$1" in
--env) ENV_ID="$2"; shift 2 ;;
--site) SITE="$2"; shift 2 ;;
--transport) TRANSPORT="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--enrol-jwt) ENROL_JWT="$2"; shift 2 ;;
--bootstrap-file) BOOTSTRAP_FILE="$2"; shift 2 ;;
--bao-ca) BAO_CA="$2"; shift 2 ;;
--base-url) BASE_URL="$2"; shift 2 ;;
--source) SOURCE="$2"; shift 2 ;;
--laptop) LAPTOP="true"; shift ;;
--no-run) NO_RUN=1; shift ;;
--force-config) FORCE_CONFIG=1; shift ;;
-h|--help) usage 0 ;;
*) die "unknown argument $1 (see --help)" ;;
esac
done
[ "$(id -u)" = 0 ] || die "run as root (sudo)"
[ -n "$ENV_ID" ] || die "--env is required"
[ -n "$SITE" ] || die "--site is required"
[[ "$ENV_ID" =~ ^env-(dev|qa|stage|prod)-[0-9]{2,3}$|^(dev-env-2|prod-cedar)$ ]] || die "env id $ENV_ID is not env-<tier>-<nn>"
SITE="${SITE,,}"
[[ "$SITE" =~ ^(cbs|pdx)$ ]] || die "site must be cbs|pdx"
[[ "$TRANSPORT" =~ ^(sdk|proxy|system)$ ]] || die "transport must be sdk|proxy|system"
[[ "$SOURCE" =~ ^(gitlab|gitea)$ ]] || die "source must be gitlab|gitea"
IDENTITY="$IDENTITY_DIR/monky-host.$ENV_ID.json"
export DEBIAN_FRONTEND=noninteractive
if [ -r /etc/os-release ]; then
. /etc/os-release
if [ "${ID:-}" != "ubuntu" ] || [ "${VERSION_ID:-}" != "26.04" ]; then
echo "WARNING: verified on Ubuntu 26.04; this is ${PRETTY_NAME:-unknown}. Continuing." >&2
fi
fi
# --- 1. packages -------------------------------------------------------------------------------------
apt_updated=""
apt_update_once() { [ -n "$apt_updated" ] || { apt-get update -qq; apt_updated=1; }; }
need_pkgs=(acl curl ca-certificates gnupg)
if ! command -v ziti-edge-tunnel >/dev/null 2>&1; then
log "adding the OpenZiti apt repository ($OPENZITI_SUITE suite)"
install -d -m 0755 /usr/share/keyrings
curl -fsSL https://get.openziti.io/tun/package-repos.gpg | gpg --dearmor -o /usr/share/keyrings/openziti.gpg
chmod 0644 /usr/share/keyrings/openziti.gpg
echo "deb [signed-by=/usr/share/keyrings/openziti.gpg] https://packages.openziti.org/zitipax-openziti-deb-stable $OPENZITI_SUITE main" \
> /etc/apt/sources.list.d/openziti.list
need_pkgs+=(ziti-edge-tunnel)
fi
if [ "$TRANSPORT" = "proxy" ] && ! command -v ziti >/dev/null 2>&1; then
need_pkgs+=(openziti) # the `ziti` CLI (ziti tunnel proxy) from the same repo
fi
command -v docker >/dev/null 2>&1 || die "docker is not installed; install Docker Engine first (https://docs.docker.com/engine/install/ubuntu/)"
docker compose version >/dev/null 2>&1 || need_pkgs+=(docker-compose-plugin)
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 12) else 1)' 2>/dev/null || die "python3 >= 3.12 is required"
if [ "${#need_pkgs[@]}" -gt 0 ]; then
log "apt install: ${need_pkgs[*]}"
apt_update_once
apt-get install -y -qq --no-install-recommends "${need_pkgs[@]}"
fi
# --- 2. the pinned .deb -----------------------------------------------------------------------------
installed="$(dpkg-query -W -f='${Version}' monky-deployd 2>/dev/null || true)"
if [ "$installed" = "$VERSION" ]; then
log "monky-deployd $VERSION already installed"
else
tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT
deb="monky-deployd_${VERSION}_amd64.deb"
if [ "$SOURCE" = gitlab ]; then
url="${BASE_URL:-$GITLAB_BASE_URL}/${VERSION}"
else
url="${BASE_URL:-$GITEA_BASE_URL}/releases/download/v${VERSION}"
fi
log "downloading $deb from $url"
curl -fsSL -o "$tmp/$deb" "$url/$deb"
curl -fsSL -o "$tmp/$deb.sha256" "$url/$deb.sha256"
(cd "$tmp" && sha256sum -c "$deb.sha256") || die "sha256 mismatch on $deb"
apt_update_once || true
apt-get install -y -qq "$tmp/$deb"
fi
command -v monky-deployd >/dev/null || die "monky-deployd not on PATH after install"
# --- 3. host identity -------------------------------------------------------------------------------
install -d -m 0750 "$IDENTITY_DIR"
[ -n "$ENROL_JWT" ] || { [ -f "$ETC/enrol.jwt" ] && ENROL_JWT="$ETC/enrol.jwt"; } || true
if [ -s "$IDENTITY" ]; then
log "host identity present: $IDENTITY"
else
[ -n "$ENROL_JWT" ] && [ -s "$ENROL_JWT" ] || die "no identity at $IDENTITY and no enrol JWT (--enrol-jwt or $ETC/enrol.jwt)"
log "enrolling monky-host.$ENV_ID"
ziti-edge-tunnel enroll -j "$ENROL_JWT" -i "$IDENTITY"
fi
getent passwd ziti >/dev/null && chown ziti:ziti "$IDENTITY" || true
chmod 0600 "$IDENTITY"
# ziti-edge-tunnel as a HOST (bind side, no tun/DNS) — the agent dials with the SDK or the proxy
install -d /etc/systemd/system/ziti-edge-tunnel.service.d
cat > /etc/systemd/system/ziti-edge-tunnel.service.d/run-host.conf <<'DROPIN'
# monky-deployd: run-host mode (no tun, no DNS); identities from the standard directory
[Service]
ExecStart=
ExecStart=/opt/openziti/bin/ziti-edge-tunnel run-host --identity-dir=/opt/openziti/etc/identities
DROPIN
systemctl daemon-reload
systemctl enable --now ziti-edge-tunnel.service
systemctl restart ziti-edge-tunnel.service || true
# --- 4. config, ACLs, bootstrap grant -----------------------------------------------------------------
install -d -m 0750 -o root -g monky-deployd "$ETC"
install -d -m 0700 -o monky-deployd -g monky-deployd /var/lib/monky-deployd
setfacl -m u:monky-deployd:r "$IDENTITY"
setfacl -m u:monky-deployd:rx "$IDENTITY_DIR"
setfacl -m u:monky-deployd:x /opt/openziti/etc 2>/dev/null || true
if [ -n "$BAO_CA" ]; then
install -m 0644 "$BAO_CA" "$ETC/openbao-ca.pem"
fi
ca_line="ca_bundle: $ETC/openbao-ca.pem"
[ -s "$ETC/openbao-ca.pem" ] || { ca_line="ca_bundle: none # TODO: install the openbao-ca certificate (see docs/OPERATIONS.md)"; echo "WARNING: $ETC/openbao-ca.pem missing; TLS to OpenBao will use the system store" >&2; }
if [ -s "$ETC/config.yaml" ] && [ -z "$FORCE_CONFIG" ]; then
log "keeping existing $ETC/config.yaml (use --force-config to rewrite)"
else
cat > "$ETC/config.yaml" <<CFG
# written by install.sh $(date -u +%FT%TZ) — monky-deployd $VERSION
env_id: $ENV_ID
site: $SITE
transport: $TRANSPORT
identity: $IDENTITY
tenancy:
service: monky.tenancy.deploy
host: monky.tenancy.deploy
port: 8081
scheme: http
proxy_addr: 127.0.0.1:18443
bao:
service: openbao
addr: https://bao.cbs.tikali.net:8200
proxy_addr: 127.0.0.1:18200
$ca_line
mount: jwt-tenancy
role: see-env
kv_mount: monky
state_dir: /var/lib/monky-deployd
deploy_dir: /var/lib/monky-deployd/$ENV_ID
bootstrap_path: $ETC/bootstrap.jwt
interval_s: 60
volumes_on_absent: keep
laptop_mode: $LAPTOP
CFG
chmod 0640 "$ETC/config.yaml"; chgrp monky-deployd "$ETC/config.yaml"
fi
if [ "$TRANSPORT" = "proxy" ]; then
printf 'ZITI_IDENTITY=%s\n' "$IDENTITY" > "$ETC/proxy.env"; chmod 0644 "$ETC/proxy.env"
systemctl enable --now monky-deployd-proxy.service
fi
# bootstrap grant: stdin (the kit pipes it) or --bootstrap-file; 0600, owned by the agent so it can consume it
grant=""
if [ -n "$BOOTSTRAP_FILE" ]; then grant="$(tr -d '[:space:]' < "$BOOTSTRAP_FILE")"
elif [ ! -t 0 ]; then grant="$(tr -d '[:space:]' </dev/stdin || true)"; fi
if [ -n "$grant" ]; then
printf '%s\n' "$grant" > "$ETC/bootstrap.jwt"
chown monky-deployd:monky-deployd "$ETC/bootstrap.jwt"; chmod 0600 "$ETC/bootstrap.jwt"
log "bootstrap grant staged at $ETC/bootstrap.jwt (consumed on first tick)"
elif [ -s /var/lib/monky-deployd/bao.token ]; then
log "no bootstrap grant given; existing bao.token kept"
else
echo "WARNING: no bootstrap grant on stdin and no bao.token: the agent cannot check in until you provide one" >&2
fi
unset grant
# --- 5. timer + first tick ----------------------------------------------------------------------------------
systemctl daemon-reload
systemctl enable --now monky-deployd.timer
if [ -z "$NO_RUN" ]; then
log "first tick"
systemctl start monky-deployd.service || true
fi
[ -n "$ENROL_JWT" ] && rm -f "$ENROL_JWT" || true
rm -f "$ETC/enrol.jwt"
cat <<CHECK
monky-deployd $VERSION installed for $ENV_ID ($SITE, transport $TRANSPORT). Verification checklist (Ubuntu 26.04):
python3 --version # >= 3.12
docker compose version # compose plugin present
systemctl is-active ziti-edge-tunnel # run-host mode (drop-in run-host.conf)
$( [ "$TRANSPORT" = proxy ] && echo "systemctl is-active monky-deployd-proxy # ziti tunnel proxy on 18443/18200" || echo "ziti tunnel proxy --help >/dev/null # fallback transport available" )
/opt/monky-deployd/venv/bin/python -c 'import openziti' # SDK import (transport sdk)
monky-deployd status # token present, applied == desired
journalctl -u monky-deployd -n 50 # 'checkin:' and 'applied' lines
df -h \$(docker info -f '{{.DockerRootDir}}') # free space >= bundle need x 1.5 + headroom
docker compose -p monky-$ENV_ID ps # healthy
# from another mesh member: curl monky.percept.$ENV_ID:47283/health -> 200
CHECK
CHECK_STATUS="$(monky-deployd status 2>&1 || true)"
echo "$CHECK_STATUS" | sed 's/^/ | /'