Fix six issues preventing a fresh production install - #314
Open
hdt-gif wants to merge 6 commits into
Open
Conversation
Three things stopped this app from starting in production, independent of where it is deployed: - gunicorn was added to INSTALLED_APPS by settings/prod.py but was never in requirements.txt, and no entrypoint invoked it. - prod.py's logging config writes to /opt/python/log/django.log, but nothing created that directory, so Django raised on startup. batch/handlers.py works around this with an explicit makedirs, which suggests it was already known. - DEBUG is False under prod.py, so Django stops serving STATIC_ROOT itself and the site renders unstyled. WhiteNoise now serves static files from inside the gunicorn process, avoiding a separate web server for single-host deploys. Also makes broker TLS configurable. CELERY_BROKER_USE_SSL was hardcoded to CERT_REQUIRED, which is correct for a managed broker with transit encryption but impossible against a plain redis container on the same host -- it fails every worker connection. It now reads from the environment and still defaults to True, so existing deployments are unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The startup script ran `loaddata --app template` before `migrate`. On a fresh database those tables do not exist yet, so the load failed -- and because the script had no `set -e`, the error was printed and ignored. The result is that reference data has never actually loaded on a new install: no abstract techs, no parameters, no template types, leaving the app unusable for building models until someone runs the documented setup steps by hand. Fixing the order alone is not safe, though. These fixtures carry hardcoded primary keys (admin_parameter.json is pk 1-154), and loaddata UPDATES the row at each id rather than skipping it. Running them unconditionally on every boot would overwrite live records -- including data restored from another Engage instance, which is exactly what this deployment needs to do. seed-reference-data.sh therefore checks first: it seeds an empty database, skips a populated one, and fails closed if it cannot tell. It also loads the api fixtures before the template ones, since template/models.py runs a pre_save hook that looks up Abstract_Tech and Parameter rows by id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE were hardcoded True. That is correct behind TLS, but on a plain-HTTP deployment the browser withholds the CSRF cookie entirely, so every form submission -- including login -- fails with "CSRF verification failed. Request aborted." Both now read from the environment and still default to True, so any existing HTTPS deployment is unchanged. Only an instance that explicitly opts out gets insecure cookies. Also adds CSRF_TRUSTED_ORIGINS, which Django needs when the form's origin is not an HTTPS host it already trusts. This is the same pattern already used for DJANGO_SECURE_SSL_REDIRECT and CELERY_BROKER_USE_SSL: safe by default, relaxable for a test box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
build_model reads its Run back from the database, but the view set run.run_options in memory and only called save() *after* apply_async. A worker that picked the task up before that save saw run_options as NULL, and get_model_yaml_set failed with "'NoneType' object is not iterable". Timing-dependent, so it depends on the machine: builds succeeded on a 4-core laptop and failed consistently on a 2-core EC2 instance, where the worker reliably won the race. Saving before dispatch closes the window. The later save() is still needed to record build_task, which cannot exist until apply_async returns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
QuerySet.first() takes no arguments, so the fallback raised
TypeError: first() takes 1 positional argument but 2 were given
rather than returning the default ComputeEnvironment. Any run started without
a valid run_env hit it and the user saw "Please contact admin at
engage@nrel.gov" instead of their model building.
The fallback exists precisely for when the requested environment cannot be
found, so it was the error path that was broken -- which is why it survived
since 2022: the UI always supplies a valid run_env, and only a request that
omits it reaches this line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Only the registration view created a User_Profile. An account made any other
way -- `createsuperuser`, the Django admin, a script -- had none, and
activity.html reads
{{ comment.created|timezone:user.user_profile.timezone }}
Django's template engine silently swallows ObjectDoesNotExist, so the missing
profile resolved to an empty string and the timezone filter raised
ValueError: ZoneInfo keys must be normalized relative paths, got:
The model page then returned a 500 for that user while working for everyone
else. `createsuperuser` is the documented way to create an admin account in the
Getting Started guide, so following the setup instructions produced an account
that could not open a model.
A post_save receiver on User closes it for every path. The timezone default is
on the field, so profiles created this way are complete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Deploying Engage from scratch onto a self-hosted server, I hit six issues that stop a fresh install running under
settings.prod. All predate this branch and none are specific to my deployment — I think anyone standing Engage up outside NREL's own environment would meet all of them.Happy to split this into separate PRs if you'd prefer.
Reference data has never loaded on a fresh install
compose/run-calliope-app.shrunsloaddata --app templatebeforemigrate. On an empty database those tables don't exist yet, so the load fails — and because the script has noset -e, the error prints and is ignored. The container then starts normally with no abstract technologies, no parameters and no template types, leaving an app you can't build models in.It also only loads the
templatefixtures. Those referenceapi.Parameterandapi.Abstract_Techrows by id, andtemplate/models.pyhas apre_savehook that looks them up, so theapifixtures have to load first or the whole batch rolls back.Fixing the order alone isn't safe, though: these fixtures carry hardcoded primary keys (
admin_parameter.jsonis pk 1–154), andloaddataupdates the row at each id rather than skipping it. Running them unconditionally on every boot would overwrite live records.compose/seed-reference-data.shtherefore checks first — seeds an empty database, skips a populated one, and fails closed if it can't tell.settings.prodcan't startThree independent problems:
INSTALLED_APPSbysettings/prod.pybut isn't inrequirements.txt, and no entrypoint invokes it/opt/python/log— the production logging handler writes there and nothing creates it, so Django raises at startup.batch/handlers.pyalready works around this with an explicitmakedirs, which suggests it was knownDEBUG=Falsemeans Django stops servingSTATIC_ROOT, so the site renders unstyled. Added WhiteNoise, which avoids requiring a separate web server for single-host deploymentsBroker TLS is not configurable
CELERY_BROKER_USE_SSLis hardcoded toCERT_REQUIRED. That's correct against a managed broker with transit encryption, but impossible against a plain Redis container on the same host — every worker connection fails. Now read from the environment, still defaulting toTrue, so existing deployments are unchanged.Secure cookies make login impossible without HTTPS
SESSION_COOKIE_SECUREandCSRF_COOKIE_SECUREare hardcodedTrue. Correct behind TLS, but on a plain-HTTP instance the browser withholds the CSRF cookie entirely and every login returns "CSRF verification failed". Also read from the environment now, still defaulting toTrue. AddsCSRF_TRUSTED_ORIGINS.Race condition in
build_modelapi/views/outputs.pysetsrun.run_optionsin memory, dispatches the Celery task, and only then callssave().build_modelre-reads the Run from the database, so a worker that starts before that save seesrun_optionsasNULLandget_model_yaml_setfails withTiming-dependent, so it depends on the machine — builds succeeded consistently on a 4-core laptop and failed consistently on a 2-core server. Fixed by saving before dispatch.
QuerySet.first()called with an argumentfirst()takes no arguments, so the fallback raisesTypeErrorinstead of returning the default environment, and the user sees "Please contact admin at engage@nrel.gov". It's the error path that's broken, which is presumably why it has survived — the UI always supplies a validrun_env, so only a request that omits it reaches this line.createsuperuserproduces an account that can't open a modelOnly the registration view creates a
User_Profile. An account made any other way has none, andactivity.htmlreads{{ comment.created|timezone:user.user_profile.timezone }}Django's template engine silently swallows
ObjectDoesNotExist, so the missing profile resolves to an empty string and the filter raisesThe model page then 500s for that user and works for everyone else. Since
createsuperuseris the documented way to create an admin account in Getting Started, following the setup guide produces an account that can't browse models. Added apost_savereceiver onUserso a profile exists regardless of how the account was created.Testing
Verified on a fresh Ubuntu 26.04 host with an empty database: reference data seeds correctly, the app starts under
settings.prodwith gunicorn, static files serve, both Celery workers connect, and the bundled national-scale sample model builds and solves — 36 seconds, 55 output files with plausible capacities and costs. Restore of apg_dumpinto a scratch database matched row counts exactly.