Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,39 @@ This should start multiple `gunicorn` workers, each one of them binding our flas

> Reminder: Replace the **`<type>`** in the commands above either with `preprint` or `preview` depending on the server (e.g., `neurolibre-preview.service`) you are configuring. Note that this is not only a naming convention, but also defines a functional separation between the roles of the two servers.

#### Isolate MyST build containers from instance metadata

Build containers execute notebook code from submitted repositories. On the default Docker bridge they can reach the instance metadata service (`169.254.169.254` on OpenStack), which serves user-data and injected credentials.

Create a dedicated network with a fixed bridge name, so the firewall rule has something stable to match:

```
docker network create --driver bridge \
--opt com.docker.network.bridge.name=br-mystbuild mystbuild
docker pull busybox:latest
```

Install the service that blocks metadata for that bridge on every boot:

```
sudo cp ~/full-stack-server/systemd/neurolibre-mystbuild-firewall.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now neurolibre-mystbuild-firewall.service
```

Verify — this is the only thing that proves the rule is working:

```
docker run --rm --network mystbuild curlimages/curl \
-s -m 3 http://169.254.169.254/openstack/ ; echo "exit=$?"
```

A non-zero exit (`7` rejected, `28` timeout) means blocked. `exit=0` with a listing of API versions means it is **not** blocked — check that `br-mystbuild` exists (`ip -o link show br-mystbuild`) and that metadata is not a local address (`ip addr | grep 169.254`, which should print nothing).

Then pass `container_network = 'mystbuild'` to `JupyterHubLocalSpawner` in `api/neurolibre_celery_tasks.py` and restart the Celery worker. myst-libre re-checks this before every build session and refuses to spawn if metadata answers, so a rule lost after a reboot fails loudly instead of silently reopening.

> Do not use `iptables-persistent` for this rule. It snapshots the entire ruleset including Docker's generated rules, and restoring those at boot before Docker starts causes duplicated and conflicting rules. The systemd unit is ordered after `docker.service` and re-adds only this rule.

#### Configure Celery as a systemd service

For Celery async task queue manager to work, there are two requirements:
Expand Down
1 change: 1 addition & 0 deletions about.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ celery -A neurolibre_celery_tasks worker --loglevel=info
The application runs as systemd services:
- `neurolibre-preview.service` - Preview server
- `neurolibre-preprint.service` - Preprint server
- `neurolibre-mystbuild-firewall.service` - Blocks instance metadata for build containers
- Celery workers for async tasks

## Key Directories
Expand Down
7 changes: 0 additions & 7 deletions api/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,13 +541,6 @@ def run_celery_subprocess(command, log_output=True):
logging.error(f"Command: {' '.join(command)}")
return -1, str(e)

def get_active_ports(start=3001, end=3099):
active_ports = []
for conn in psutil.net_connections(kind='inet'):
if conn.status == psutil.CONN_LISTEN and start <= conn.laddr.port <= end:
active_ports.append(conn.laddr.port)
return active_ports

def close_port_by_pid(target_pid):
"""Kill the entire process group rooted at target_pid.

Expand Down
27 changes: 26 additions & 1 deletion api/github_client.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import os
import re
from common import get_time
import logging
from common import get_time, parse_front_matter
import json
import yaml
import git
from myst_frontmatter import merge_paper_metadata

# Name of the GitHub organization where repositories
# will be forked into for production. Editorial bot
Expand Down Expand Up @@ -330,6 +332,29 @@ def gh_get_paper_markdown(github_client,repo):
file_content = gh_get_file_content(github_client,repo,"paper.md")
return file_content

def gh_get_paper_metadata(github_client, repo):
"""Paper metadata for a submission, with myst.yml filling any gaps.

NeuroLibre requires myst.yml at the repository root beside paper.md, so a
submission need not repeat its title, authors, and affiliations in the
paper.md front matter. Returns None only when neither source names an
author.

This runs before the repository is cloned, so both files are fetched
through the GitHub API rather than read from disk.
"""
paper = gh_get_file_content(github_client, repo, "paper.md")

front_matter = None
if paper:
try:
front_matter = parse_front_matter(paper)
except yaml.YAMLError as error:
logging.warning(f"Could not parse paper.md front matter: {error}")

myst = gh_get_file_content(github_client, repo, "myst.yml")
return merge_paper_metadata(front_matter, myst)

def gh_read_from_issue_body(github_client,issue_repo,issue_id,tag):
"""
Issue body of the reviews has markers around review entries
Expand Down
Loading
Loading