diff --git a/README.md b/README.md index 2bd560f..cbfd352 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,18 @@ Run migrations and start the development server: uv run python manage.py migrate uv run python manage.py runserver +`mise.toml` sets `DJANGO_DEBUG=1`; without it `DEBUG` is off. Local overrides go in +`mdbsite/local_settings.py`. + +## Production + +MDB runs on sega under edb-infra's `mdb` role (`roles/mdb.md` there): gunicorn from a +release directory per commit, its settings in the file `MDB_SETTINGS` names +(`/etc/mdb/settings.py`), which `mdbsite/settings.py` executes last. A push to `main` that +passes the checks deploys itself while the repository variable `DEPLOY_SEGA` is `true` +(`.gitea/workflows/deploy.yaml`). The DNS and DHCP sync runs as root on sega: +`sudo mdb-sync`. + ## Development tasks Dump test data: diff --git a/mdbsite/settings.py b/mdbsite/settings.py index 8b5f049..ba557e6 100644 --- a/mdbsite/settings.py +++ b/mdbsite/settings.py @@ -2,9 +2,10 @@ import os BASE_DIR = os.path.dirname(os.path.dirname(__file__)) -DEBUG = os.getenv("DJANGO_DEBUG", True) +# Off unless DJANGO_DEBUG says otherwise; mise.toml turns it on for development. +DEBUG = os.getenv("DJANGO_DEBUG", "").lower() in ("1", "true", "yes") -ADMINS = (("System Administrator", "sysadmin@example.com"),) +ADMINS = ("sysadmin@example.com",) MANAGERS = ADMINS @@ -35,10 +36,12 @@ STATICFILES_FINDERS = ( "django.contrib.staticfiles.finders.AppDirectoriesFinder", ) -# Make this unique, and don't share it with anybody. +# For development only: the repository is public. Production sets its own (MDB_SETTINGS). SECRET_KEY = "@4ps=ebofcpv8sb#y7gm896=l&l*u0u-pgo7bcdj%+(k+2lqnl" MIDDLEWARE = ( + # Serves STATIC_ROOT, so production needs nothing in front but a proxy. + "whitenoise.middleware.WhiteNoiseMiddleware", "django.middleware.common.CommonMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.csrf.CsrfViewMiddleware", @@ -132,7 +135,16 @@ SENTRY_ENVIRONMENT = os.getenv("SENTRY_ENVIRONENT", "dev") ADMIN_SITE_NAME = "MDB" -try: - from .local_settings import * -except ImportError: - pass +# Production's settings live outside the code, in the file MDB_SETTINGS names; the mdb role +# in edb-infra writes it. An error in it stops MDB rather than leaving the defaults above. +if os.getenv("MDB_SETTINGS"): + with open(os.environ["MDB_SETTINGS"]) as f: + exec(compile(f.read(), os.environ["MDB_SETTINGS"], "exec")) +else: + # Development. Only a missing local_settings is skipped: a local_settings whose own + # imports fail must not leave MDB running on the defaults. + try: + from .local_settings import * # noqa: F403 + except ModuleNotFoundError as e: + if e.name != "mdbsite.local_settings": + raise diff --git a/mise.toml b/mise.toml index 81daabc..2574a0e 100644 --- a/mise.toml +++ b/mise.toml @@ -1,3 +1,7 @@ [tools] python = "3.14" uv = "latest" + +[env] +# settings.py leaves DEBUG off unless this says otherwise. +DJANGO_DEBUG = "1" diff --git a/pyproject.toml b/pyproject.toml index a219886..abad2f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,10 +15,11 @@ dependencies = [ "ipaddress>=1.0.23,<2.0.0", "requests>=2.32.5", "sentry-sdk>=2.55.0", + "whitenoise>=6.12,<7", ] [dependency-groups] -# Only needed on the server (LDAP is configured in local_settings.py). +# Only needed on the server (LDAP is configured in production's settings). # Kept separate so CI can skip building python-ldap from source. prod = [ "django-auth-ldap>=5.3.0", diff --git a/tests/test_settings.py b/tests/test_settings.py new file mode 100644 index 0000000..36cb0bb --- /dev/null +++ b/tests/test_settings.py @@ -0,0 +1,57 @@ +"""How mdbsite.settings loads production's settings, in a fresh interpreter each time, +since the settings module is imported once per process.""" + +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + +PRINT = ( + "import django, os; os.environ['DJANGO_SETTINGS_MODULE'] = 'mdbsite.settings'; " + "django.setup(); from django.conf import settings as s; print(s.ALLOWED_HOSTS, s.DEBUG)" +) + + +def run(env): + return subprocess.run( + [sys.executable, "-c", PRINT], + cwd=ROOT, + env={ + k: v + for k, v in os.environ.items() + if k not in ("MDB_SETTINGS", "DJANGO_DEBUG") + } + | env, + capture_output=True, + text=True, + ) + + +def test_settings_file_overrides(tmp_path): + f = tmp_path / "settings.py" + f.write_text("ALLOWED_HOSTS = ['mdb.example.org']\n") + result = run({"MDB_SETTINGS": str(f)}) + assert result.returncode == 0, result.stderr + assert result.stdout.strip() == "['mdb.example.org'] False" + + +def test_failing_settings_file_stops_mdb(tmp_path): + # As on sega after the 24.04 upgrade: python-ldap would not load, and the old + # `except ImportError: pass` left MDB running on the defaults. + f = tmp_path / "settings.py" + f.write_text("import a_module_that_does_not_exist\n") + result = run({"MDB_SETTINGS": str(f)}) + assert result.returncode != 0 + assert "a_module_that_does_not_exist" in result.stderr + + +def test_missing_settings_file_stops_mdb(tmp_path): + result = run({"MDB_SETTINGS": str(tmp_path / "missing.py")}) + assert result.returncode != 0 + + +def test_debug_is_off_unless_asked(): + assert run({}).stdout.strip().endswith("False") + assert run({"DJANGO_DEBUG": "1"}).stdout.strip().endswith("True") diff --git a/uv.lock b/uv.lock index 6541af6..6651d36 100644 --- a/uv.lock +++ b/uv.lock @@ -185,6 +185,7 @@ dependencies = [ { name = "ipaddress" }, { name = "requests" }, { name = "sentry-sdk" }, + { name = "whitenoise" }, ] [package.dev-dependencies] @@ -207,6 +208,7 @@ requires-dist = [ { name = "ipaddress", specifier = ">=1.0.23,<2.0.0" }, { name = "requests", specifier = ">=2.32.5" }, { name = "sentry-sdk", specifier = ">=2.55.0" }, + { name = "whitenoise", specifier = ">=6.12,<7" }, ] [package.metadata.requires-dev] @@ -404,3 +406,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e wheels = [ { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, ] + +[[package]] +name = "whitenoise" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, +]