Skip to content

Commit d2ddf79

Browse files
committed
Add gunicorn & CMEMS
1 parent b0556bd commit d2ddf79

9 files changed

Lines changed: 132 additions & 37 deletions

File tree

opendrift/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
FROM opendrift/opendrift
2-
RUN pip3 install OWSLib
2+
RUN pip3 install OWSLib copernicusmarine --upgrade
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
ASGI config for django_leeway project.
3+
4+
It exposes the ASGI callable as a module-level variable named ``application``.
5+
"""
6+
7+
import configparser
8+
import os
9+
10+
from django.core.asgi import get_asgi_application
11+
12+
os.environ.setdefault(
13+
"DJANGO_SETTINGS_MODULE", "opendrift_leeway_webgui.core.settings"
14+
)
15+
16+
# Read config from config file
17+
config = configparser.ConfigParser(interpolation=None)
18+
config.read("/etc/opendrift-leeway-webgui.ini")
19+
for section in config.sections():
20+
for key, value in config.items(section):
21+
os.environ.setdefault(f"LEEWAY_{key.upper()}", value)
22+
23+
application = get_asgi_application()

opendrift_leeway_webgui/core/settings.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
x.strip() for x in os.environ.get("LEEWAY_ALLOWED_HOSTS", "").split(",") if x
5353
]
5454

55+
CSRF_TRUSTED_ORIGINS = [
56+
f"https://{host}" for host in ALLOWED_HOSTS
57+
]
58+
5559
#: Enabled applications (see :setting:`django:INSTALLED_APPS`)
5660
INSTALLED_APPS = [
5761
"django.contrib.admin",
@@ -240,6 +244,10 @@
240244
"class": "logging.StreamHandler",
241245
"formatter": "console-colored",
242246
},
247+
"console-always": {
248+
"class": "logging.StreamHandler",
249+
"formatter": "console-colored",
250+
},
243251
"logfile": {
244252
"class": "logging.FileHandler",
245253
"filename": LOGFILE,
@@ -249,11 +257,11 @@
249257
"loggers": {
250258
# Loggers of opendrift-leeway-webgui django apps
251259
"opendrift_leeway_webgui": {
252-
"handlers": ["console-colored", "logfile"],
260+
"handlers": ["console-always", "logfile"],
253261
"level": LOG_LEVEL,
254262
},
255263
# Loggers of dependencies
256-
"celery": {"handlers": ["console", "logfile"], "level": DEPS_LOG_LEVEL},
264+
"celery": {"handlers": ["console-always", "logfile"], "level": "INFO"},
257265
"django": {"handlers": ["console", "logfile"], "level": DEPS_LOG_LEVEL},
258266
"dms2dec": {"handlers": ["console", "logfile"], "level": DEPS_LOG_LEVEL},
259267
"redis": {"handlers": ["console", "logfile"], "level": DEPS_LOG_LEVEL},
@@ -365,3 +373,6 @@
365373
#: In most email documentation this type of TLS connection is referred to as SSL. It is generally used on port 465.
366374
#: (see :setting:`django:EMAIL_USE_SSL`)
367375
EMAIL_USE_SSL = bool(strtobool(os.environ.get("LEEWAY_EMAIL_USE_SSL", "False")))
376+
377+
COPERNICUSMARINE_SERVICE_USERNAME = os.environ.get("LEEWAY_COPERNICUSMARINE_SERVICE_USERNAME")
378+
COPERNICUSMARINE_SERVICE_PASSWORD = os.environ.get("LEEWAY_COPERNICUSMARINE_SERVICE_PASSWORD")

opendrift_leeway_webgui/leeway/tasks.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ def run_leeway_simulation(request_id):
2727
params = [
2828
"docker",
2929
"run",
30+
"-e",
31+
f"COPERNICUSMARINE_SERVICE_USERNAME={settings.COPERNICUSMARINE_SERVICE_USERNAME}",
32+
"-e",
33+
f"COPERNICUSMARINE_SERVICE_PASSWORD={settings.COPERNICUSMARINE_SERVICE_PASSWORD}",
34+
"-e",
35+
f"COPERNICUSMARINE_USERNAME={settings.COPERNICUSMARINE_SERVICE_USERNAME}",
36+
"-e",
37+
f"COPERNICUSMARINE_PASSWORD={settings.COPERNICUSMARINE_SERVICE_PASSWORD}",
3038
"--volume",
3139
f"{settings.SIMULATION_ROOT}:/code/leeway",
3240
"--volume",
@@ -51,11 +59,12 @@ def run_leeway_simulation(request_id):
5159
"--id",
5260
str(simulation.uuid),
5361
]
62+
5463
with subprocess.Popen(
5564
params, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True
5665
) as sim_proc:
5766
stdout, stderr = sim_proc.communicate()
58-
logger.debug("Output from simulation %s: %s", simulation.uuid, stdout)
67+
logger.info("Output from simulation %s: %s", simulation.uuid, stdout)
5968
if stderr:
6069
logger.error("Error during simulation %s: %s", simulation.uuid, stderr)
6170
simulation.traceback = stderr.strip()

opendrift_leeway_webgui/leeway/templates/leeway/leewaysimulation_form.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
<h2>New Leeway Simulation</h2>
44
<p>Please note that only simulations +/- 5 days from now are possible.</p>
55
<form method="post">
6+
{% csrf_token %}
67
<table>
7-
{% csrf_token %}
88
{{ form.as_table }}
99
</table>
1010
<button type="submit">Simulate</button>

opendrift_leeway_webgui/leeway/urls.py

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
For more information on this file, see :doc:`django:topics/http/urls`.
66
"""
77

8-
from django.conf import settings
9-
from django.conf.urls.static import static
108
from django.urls import include, path
119

1210
from .views import (
@@ -16,6 +14,7 @@
1614
LeewaySimulationDetailView,
1715
LeewaySimulationDocumentation,
1816
LeewaySimulationListView,
17+
SimulationFileView,
1918
)
2019

2120
#: The url patterns of this module (see :doc:`django:topics/http/urls`)
@@ -49,19 +48,6 @@
4948
),
5049
]
5150

52-
# Serve simulation files in debug mode
53-
if settings.DEBUG:
54-
urlpatterns += [
55-
path(
56-
"",
57-
include(
58-
(
59-
static(
60-
settings.SIMULATION_URL,
61-
document_root=settings.SIMULATION_OUTPUT,
62-
),
63-
"simulation_files",
64-
)
65-
),
66-
)
67-
]
51+
urlpatterns += [
52+
path("simulation-files/<path:path>", SimulationFileView.as_view(), name="simulation_file"),
53+
]

opendrift_leeway_webgui/leeway/views.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import mimetypes
2+
from pathlib import Path
3+
14
from django.conf import settings
25
from django.contrib import messages
36
from django.contrib.auth.mixins import LoginRequiredMixin
4-
from django.http import HttpResponseForbidden, HttpResponseRedirect
7+
from django.http import FileResponse, Http404, HttpResponseForbidden, HttpResponseRedirect
58
from django.urls import reverse_lazy
69
from django.views.generic import TemplateView
7-
from django.views.generic.base import RedirectView
10+
from django.views.generic.base import RedirectView, View
811
from django.views.generic.detail import DetailView
912
from django.views.generic.edit import CreateView, DeleteView
1013
from django.views.generic.list import ListView
@@ -118,3 +121,38 @@ def get(self, request, *args, **kwargs):
118121
self.get_object().delete()
119122
return HttpResponseRedirect("/simulations")
120123
return HttpResponseForbidden("Cannot delete other's simulations")
124+
125+
126+
class SimulationFileView(LoginRequiredMixin, View):
127+
"""
128+
Serve simulation output files (PNG, NetCDF) with login and ownership protection.
129+
130+
The filename stem must match the UUID of an existing simulation owned by the
131+
requesting user. Any other request is rejected with 403 or 404.
132+
"""
133+
134+
def get(self, request, path):
135+
"""
136+
Serve the requested simulation file if the user owns it.
137+
"""
138+
file_path = Path(settings.SIMULATION_OUTPUT) / path
139+
140+
# Only allow a flat filename — no directory traversal
141+
if Path(path).parent != Path("."):
142+
raise Http404
143+
144+
if not file_path.is_file():
145+
raise Http404
146+
147+
# Extract UUID from filename stem and verify ownership
148+
uuid_str = file_path.stem
149+
try:
150+
simulation = LeewaySimulation.objects.get(uuid=uuid_str)
151+
except (LeewaySimulation.DoesNotExist, ValueError):
152+
raise Http404
153+
154+
if simulation.user != request.user:
155+
return HttpResponseForbidden("You do not have permission to access this file.")
156+
157+
content_type, _ = mimetypes.guess_type(str(file_path))
158+
return FileResponse(file_path.open("rb"), content_type=content_type or "application/octet-stream")

opendrift_leeway_webgui/simulation.py

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import argparse
1212
import os
13+
import sys
1314
import uuid
1415
from datetime import datetime, timedelta
1516

@@ -25,8 +26,10 @@
2526
from matplotlib.colors import ListedColormap
2627

2728
# pylint: disable=import-error, disable=no-name-in-module
29+
import copernicusmarine
2830
from opendrift.models.leeway import Leeway
2931
from opendrift.readers import reader_global_landmask
32+
from opendrift.readers.reader_netCDF_CF_generic import Reader
3033

3134
INPUTDIR = "/code/leeway/input"
3235

@@ -85,23 +88,46 @@ def main():
8588
args = parser.parse_args()
8689

8790
simulation = Leeway(loglevel=50)
88-
sources = [
91+
92+
local_sources = [
8993
os.path.join(INPUTDIR, data_file)
9094
for data_file in os.listdir(INPUTDIR)
9195
if data_file.endswith(".nc")
9296
]
93-
94-
if not args.no_web:
95-
sources.extend(
96-
(
97-
"https://tds.hycom.org/thredds/dodsC/GLBy0.08/latest",
97+
if local_sources:
98+
sources = local_sources
99+
elif not args.no_web:
100+
if "COPERNICUSMARINE_SERVICE_USERNAME" in os.environ:
101+
print("Using CMEMS")
102+
cmems_dataset_ids = [
103+
"cmems_mod_med_phy-cur_anfc_4.2km_PT15M-i",
104+
"cmems_mod_glo_phy_anfc_merged-uv_PT1H-i",
105+
"cmems_obs-wind_glo_phy_nrt_l4_0.125deg_PT1H",
106+
]
107+
print("Using sources:\n - {}".format("\n - ".join(cmems_dataset_ids)))
108+
readers = []
109+
for dataset_id in cmems_dataset_ids:
110+
try:
111+
ds = copernicusmarine.open_dataset(dataset_id=dataset_id, chunk_size_limit=0)
112+
print(f"Opened {dataset_id}:")
113+
print(ds)
114+
except Exception as exc:
115+
print(f"ERROR opening {dataset_id}: {exc}", file=sys.stderr)
116+
raise
117+
readers.append(Reader(ds, name=dataset_id))
118+
simulation.add_reader(readers)
119+
sources = [
98120
"https://pae-paha.pacioos.hawaii.edu/thredds/dodsC/ncep_global/NCEP_Global_Atmospheric_Model_best.ncd",
99-
)
100-
)
101-
102-
print("Using sources:\n - {}".format("\n - ".join(sources)))
103-
104-
simulation.add_readers_from_list(sources, lazy=True)
121+
]
122+
simulation.add_readers_from_list(sources, lazy=False)
123+
124+
else:
125+
sources = [
126+
"https://tds.hycom.org/thredds/dodsC/GLBy0.08/latest",
127+
"https://pae-paha.pacioos.hawaii.edu/thredds/dodsC/ncep_global/NCEP_Global_Atmospheric_Model_best.ncd",
128+
]
129+
print("Using sources:\n - {}".format("\n - ".join(sources)))
130+
simulation.add_readers_from_list(sources, lazy=False)
105131

106132
reader_landmask = reader_global_landmask.Reader()
107133
simulation.add_reader([reader_landmask])

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ dependencies = [
2525
"dms2dec",
2626
"drf-spectacular",
2727
"redis",
28+
"uvicorn",
29+
"gunicorn"
2830
]
2931

3032
[project.urls]

0 commit comments

Comments
 (0)