pip_install's `unless` (import l4d2host, l4d2web) skips when both packages are already installed — so on a code-only apply, pip_install doesn't fire and alembic_upgrade (which it triggers) never runs. The new 0008 migration would silently get skipped, leaving the DB out of sync with the new schema. Wire git_deploy → alembic_upgrade directly. alembic upgrade head is idempotent (no-op when at head); seed_overlays + service:restart cascade off alembic, so editable-install code changes also get picked up by gunicorn. Edge case noted (deferred): a migration-only change with no code change has the same matching git rev, so this won't fire either. In practice migrations always come with the code change that uses them.
255 lines
8.3 KiB
Python
255 lines
8.3 KiB
Python
# Items for the left4me bundle.
|
|
# Systemd units come from metadata via bundles/systemd/ — there are no
|
|
# .service or .slice files in this bundle's files/ tree.
|
|
|
|
directories = {
|
|
'/opt/left4me': {
|
|
'owner': 'left4me',
|
|
'group': 'left4me',
|
|
},
|
|
'/opt/left4me/src': {
|
|
'owner': 'left4me',
|
|
'group': 'left4me',
|
|
},
|
|
'/etc/left4me': {
|
|
'owner': 'root',
|
|
'group': 'root',
|
|
'mode': '0755',
|
|
},
|
|
'/var/lib/left4me': {
|
|
# left4me's home dir — useradd creates with 0700; loosen to 0711 so
|
|
# l4d2-sandbox can traverse (but not list) for bwrap bind-mounts.
|
|
'owner': 'left4me',
|
|
'group': 'left4me',
|
|
'mode': '0711',
|
|
},
|
|
'/var/lib/left4me/installation': {'owner': 'left4me', 'group': 'left4me'},
|
|
'/var/lib/left4me/overlays': {'owner': 'left4me', 'group': 'left4me'},
|
|
'/var/lib/left4me/instances': {'owner': 'left4me', 'group': 'left4me'},
|
|
'/var/lib/left4me/runtime': {'owner': 'left4me', 'group': 'left4me'},
|
|
'/var/lib/left4me/workshop_cache': {'owner': 'left4me', 'group': 'left4me'},
|
|
'/var/lib/left4me/tmp': {'owner': 'left4me', 'group': 'left4me'},
|
|
'/usr/local/libexec/left4me': {
|
|
'owner': 'root',
|
|
'group': 'root',
|
|
'mode': '0755',
|
|
},
|
|
}
|
|
|
|
groups = {
|
|
'left4me': {'gid': 980},
|
|
'l4d2-sandbox': {'gid': 981},
|
|
}
|
|
|
|
users = {
|
|
'left4me': {
|
|
'uid': 980,
|
|
'gid': 980,
|
|
'home': '/var/lib/left4me',
|
|
'shell': '/usr/sbin/nologin',
|
|
},
|
|
'l4d2-sandbox': {
|
|
'uid': 981,
|
|
'gid': 981,
|
|
'shell': '/usr/sbin/nologin',
|
|
},
|
|
}
|
|
# UIDs/GIDs pinned in the system-package range (100-999, per Debian
|
|
# policy) so file ownership is deterministic across rebuilds and
|
|
# backup restores. 980/981 are unused elsewhere in this repo.
|
|
|
|
# Privileged helpers (mode 0755 root:root). Listed by sudoers as the only
|
|
# commands left4me can invoke as root NOPASSWD.
|
|
HELPERS = (
|
|
'left4me-systemctl',
|
|
'left4me-journalctl',
|
|
'left4me-overlay',
|
|
'left4me-script-sandbox',
|
|
)
|
|
|
|
files = {
|
|
'/usr/local/sbin/left4me': {
|
|
'source': 'usr/local/sbin/left4me', # explicit — basename collides with sudoers
|
|
'mode': '0755',
|
|
'owner': 'root',
|
|
'group': 'root',
|
|
},
|
|
**{
|
|
f'/usr/local/libexec/left4me/{h}': {
|
|
'source': f'usr/local/libexec/left4me/{h}',
|
|
'mode': '0755',
|
|
'owner': 'root',
|
|
'group': 'root',
|
|
}
|
|
for h in HELPERS
|
|
},
|
|
'/etc/left4me/sandbox-resolv.conf': {
|
|
'source': 'etc/left4me/sandbox-resolv.conf',
|
|
'mode': '0644',
|
|
'owner': 'root',
|
|
'group': 'root',
|
|
},
|
|
'/etc/sudoers.d/left4me': {
|
|
'source': 'etc/sudoers.d/left4me',
|
|
'mode': '0440',
|
|
'owner': 'root',
|
|
'group': 'root',
|
|
'test_with': 'visudo -cf {}',
|
|
},
|
|
'/etc/sysctl.d/99-left4me.conf': {
|
|
'source': 'etc/sysctl.d/99-left4me.conf',
|
|
'mode': '0644',
|
|
'owner': 'root',
|
|
'group': 'root',
|
|
'triggers': [
|
|
'action:left4me_sysctl_reload',
|
|
],
|
|
},
|
|
'/etc/left4me/host.env': {
|
|
'source': 'etc/left4me/host.env.mako',
|
|
'content_type': 'mako',
|
|
'mode': '0644',
|
|
'owner': 'root',
|
|
'group': 'root',
|
|
},
|
|
'/etc/left4me/web.env': {
|
|
'source': 'etc/left4me/web.env.mako',
|
|
'content_type': 'mako',
|
|
'mode': '0640',
|
|
'owner': 'root',
|
|
'group': 'left4me',
|
|
'needs': [
|
|
'group:left4me',
|
|
],
|
|
},
|
|
}
|
|
|
|
actions = {
|
|
'left4me_sysctl_reload': {
|
|
'command': 'sysctl --system >/dev/null',
|
|
'triggered': True,
|
|
},
|
|
}
|
|
|
|
git_deploy = {
|
|
'/opt/left4me/src': {
|
|
'repo': node.metadata.get('left4me/git_url'),
|
|
'rev': node.metadata.get('left4me/git_branch'),
|
|
'triggers': [
|
|
# On a code-update apply, refresh the DB schema. pip_install
|
|
# would have triggered alembic in the create_venv path, but on
|
|
# a normal apply pip_install's `unless` skips (packages still
|
|
# importable from the previous editable install), and that
|
|
# would leave alembic_upgrade dormant. Wiring git_deploy →
|
|
# alembic directly ensures new migrations land whenever new
|
|
# code lands. alembic upgrade head is idempotent (no-op when
|
|
# already at head), so this is safe to fire on every code
|
|
# update; the seed_overlays + service:restart cascade off
|
|
# alembic also covers picking up the new code in gunicorn.
|
|
'action:left4me_alembic_upgrade',
|
|
],
|
|
# chown_src and pip_install are NOT in triggers — they run every
|
|
# apply gated by their own `unless` guards, which makes the chain
|
|
# self-healing after a partial failure. (Items in a triggers list
|
|
# must be triggered:True, which would lose that property.)
|
|
},
|
|
}
|
|
|
|
actions['left4me_chown_src'] = {
|
|
# Runs every apply (cheap — chown -R on a small tree). Self-heals
|
|
# whenever git_deploy extracts a new tarball as root-owned files.
|
|
# Not in any triggers list so doesn't need triggered:True.
|
|
'command': 'chown -R left4me:left4me /opt/left4me/src',
|
|
'unless': 'test -z "$(find /opt/left4me/src \\! -user left4me -print -quit 2>/dev/null)"',
|
|
'cascade_skip': False,
|
|
'needs': [
|
|
'git_deploy:/opt/left4me/src',
|
|
'user:left4me',
|
|
'group:left4me',
|
|
],
|
|
}
|
|
|
|
actions['left4me_create_venv'] = {
|
|
'command': 'sudo -u left4me /usr/bin/python3 -m venv /opt/left4me/.venv',
|
|
'unless': 'test -x /opt/left4me/.venv/bin/python',
|
|
'cascade_skip': False,
|
|
'needs': [
|
|
'directory:/opt/left4me',
|
|
'pkg_apt:python3-venv',
|
|
'user:left4me',
|
|
],
|
|
'triggers': [
|
|
'action:left4me_pip_upgrade',
|
|
],
|
|
}
|
|
|
|
actions['left4me_pip_upgrade'] = {
|
|
'command': 'sudo -u left4me /opt/left4me/.venv/bin/python -m pip install --upgrade pip',
|
|
'triggered': True,
|
|
'cascade_skip': False,
|
|
'needs': [
|
|
'pkg_apt:python3-pip',
|
|
],
|
|
# No triggers — pip_install runs on every apply (gated by `unless`)
|
|
# rather than being chained from here. Keeps pip_upgrade scoped to
|
|
# exactly its purpose.
|
|
}
|
|
|
|
actions['left4me_pip_install'] = {
|
|
# Single pip invocation installs both editable packages from the same
|
|
# checkout. Runs on every apply and self-heals after partial failures;
|
|
# `unless` short-circuits when both packages are already importable.
|
|
'command': 'sudo -u left4me /opt/left4me/.venv/bin/pip install -e /opt/left4me/src/l4d2host -e /opt/left4me/src/l4d2web',
|
|
'unless': 'sudo -u left4me /opt/left4me/.venv/bin/python -c "import l4d2host, l4d2web"',
|
|
'cascade_skip': False,
|
|
'needs': [
|
|
'git_deploy:/opt/left4me/src',
|
|
'action:left4me_create_venv',
|
|
'action:left4me_chown_src',
|
|
],
|
|
'triggers': [
|
|
'action:left4me_alembic_upgrade',
|
|
],
|
|
}
|
|
|
|
actions['left4me_alembic_upgrade'] = {
|
|
# Mirrors deploy-test-server.sh:239-242. Runs as left4me with both env
|
|
# files sourced; JOB_WORKER_ENABLED=false so a stray worker doesn't race
|
|
# with the migration.
|
|
'command': (
|
|
'sudo -u left4me sh -c "'
|
|
'cd /opt/left4me/src/l4d2web && '
|
|
'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && '
|
|
'env JOB_WORKER_ENABLED=false PYTHONPATH=/opt/left4me/src '
|
|
'/opt/left4me/.venv/bin/alembic -c /opt/left4me/src/l4d2web/alembic.ini upgrade head'
|
|
'"'
|
|
),
|
|
'triggered': True,
|
|
'cascade_skip': False,
|
|
'needs': [
|
|
'action:left4me_pip_install',
|
|
'file:/etc/left4me/host.env',
|
|
'file:/etc/left4me/web.env',
|
|
],
|
|
'triggers': [
|
|
'action:left4me_seed_overlays',
|
|
'svc_systemd:left4me-web.service:restart',
|
|
],
|
|
}
|
|
|
|
actions['left4me_seed_overlays'] = {
|
|
# Idempotent: refreshes script bodies in place; existing overlay rows keep their ids.
|
|
'command': (
|
|
'sudo -u left4me sh -c "'
|
|
'set -a && . /etc/left4me/host.env && . /etc/left4me/web.env && set +a && '
|
|
'env JOB_WORKER_ENABLED=false PYTHONPATH=/opt/left4me/src '
|
|
'/opt/left4me/.venv/bin/flask --app l4d2web.app:create_app '
|
|
'seed-script-overlays /opt/left4me/src/examples/script-overlays'
|
|
'"'
|
|
),
|
|
'triggered': True,
|
|
'cascade_skip': False,
|
|
'needs': [
|
|
'action:left4me_alembic_upgrade',
|
|
],
|
|
}
|