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
28 changes: 28 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.git
.gitignore
.venv
venv
__pycache__
*.pyc
*.pyo
.env
.env.*
*.egg-info
dist
build
node_modules
.mypy_cache
.pytest_cache
.ruff_cache
*.md
!README.md
Dockerfile
docker-compose*.yml
.dockerignore
.github
tests
*.toml
!pyproject.toml
.gitignore
AGENTS.md
LICENSE
4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,6 @@ target/
# Jupyter Notebook
.ipynb_checkpoints

# pyenv
.python-version

# celery beat schedule file
celerybeat-schedule

Expand All @@ -85,6 +82,7 @@ celerybeat-schedule
# Environments
.env
.venv
uv.lock
env/
venv/
ENV/
Expand Down
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.14
32 changes: 0 additions & 32 deletions .travis.yml

This file was deleted.

118 changes: 78 additions & 40 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,42 +1,80 @@
# This Dockerfile is used for standalone installs.
# ============================================================
# Stage 1: Builder — install dependencies in an isolated layer
# ============================================================
FROM python:3.14-slim-bookworm AS builder

# Install uv for ultra-fast dependency resolution
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

# Copy only dependency manifests first (layer cache optimization)
COPY pyproject.toml uv.lock ./

# Install dependencies into the system Python (no venv needed in container)
RUN uv sync --frozen --no-dev --no-install-project

# Copy application code
COPY . .

# Install the project itself
RUN uv sync --frozen --no-dev

# ============================================================
# Stage 2: Go Builder — build test clients
# ============================================================
FROM golang:1.23-bookworm AS go-builder

RUN apt-get update && apt-get install -y git

# Build ndt7, ndt5 and dash Go clients.
FROM golang:1.17-bullseye AS build
RUN apt-get update
RUN apt-get install -y git
ENV GO111MODULE=on
RUN go get github.com/neubot/dash/cmd/dash-client@master
RUN go get github.com/m-lab/ndt7-client-go/cmd/ndt7-client
RUN go get github.com/m-lab/ndt5-client-go/cmd/ndt5-client

# Murakami image
FROM python:3.7-bullseye
# Install dependencies, speedtest and ooniprobe.
# For ooniprobe, see https://ooni.org/install/cli/ubuntu-debian for instructions
RUN apt-key adv --verbose --keyserver hkp://keyserver.ubuntu.com --recv-keys 'B5A08F01796E7F521861B449372D1FF271F2DD50'
RUN echo "deb http://deb.ooni.org/ unstable main" | tee /etc/apt/sources.list.d/ooniprobe.list
RUN apt-get update
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get install -yq git gcc libc-dev libffi-dev libssl-dev make rustc cargo ooniprobe-cli curl
RUN /usr/local/bin/python3.7 -m pip install --upgrade pip
RUN curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | bash
RUN apt-get install speedtest
RUN pip install 'poetry==1.1.7'

WORKDIR /murakami

# Copy Murakami and previously built test clients into the container.
COPY . /murakami/
COPY --from=build /go/bin/* /murakami/bin/

COPY ./configs/speedtest-cli.json /root/.config/ookla/

# Set up poetry to not create a virtualenv, since the docker container is
# isolated already, and install the required dependencies.
RUN poetry config virtualenvs.create false \
&& poetry install --no-dev --no-interaction

# Add binaries' path to PATH.
ENV PATH="/murakami/bin:${PATH}"

ENTRYPOINT [ "python", "-m", "murakami" ]

RUN go install github.com/neubot/dash/cmd/dash-client@master && \
go install github.com/m-lab/ndt7-client-go/cmd/ndt7-client && \
go install github.com/m-lab/ndt5-client-go/cmd/ndt5-client

# ============================================================
# Stage 3: Runtime — minimal image with only what's needed
# ============================================================
FROM python:3.14-slim-bookworm AS runtime

# Install system dependencies
RUN apt-key adv --verbose --keyserver hkp://keyserver.ubuntu.com --recv-keys 'B5A08F01796E7F521861B449372D1FF271F2DD50' && \
echo "deb http://deb.ooni.org/ unstable main" | tee /etc/apt/sources.list.d/ooniprobe.list && \
apt-get update && \
apt-get install -y --no-install-recommends \
git gcc libc-dev libffi-dev libssl-dev make rustc cargo curl \
ooniprobe-cli && \
curl -s https://packagecloud.io/install/repositories/ookla/speedtest-cli/script.deb.sh | bash && \
apt-get install -y speedtest && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*

# Security: run as non-root
RUN groupadd --gid 1000 appuser && \
useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser

WORKDIR /app

# Copy the virtual environment from builder
COPY --from=builder /app/.venv /app/.venv

# Copy Go binaries
COPY --from=go-builder /go/bin /app/bin

# Copy application code
COPY --from=builder /app .

# Copy speedtest config
COPY ./configs/speedtest-cli.json /home/appuser/.config/ookla/

# Put the venv on PATH
ENV PATH="/app/.venv/bin:/app/bin:${PATH}"

# Add binaries' path to PATH
ENV PYTHONPATH="/app"

# Drop privileges
USER appuser

ENTRYPOINT ["python", "-m", "murakami"]
43 changes: 0 additions & 43 deletions Dockerfile.template

This file was deleted.

1 change: 1 addition & 0 deletions murakami/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
one or more remote servers via SCP, or to a Google Cloud Storage bucket.
Results are saved as individual files in JSON new line format (.jsonl).
"""

__version__ = "0.1.0"
37 changes: 23 additions & 14 deletions murakami/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
one or more remote servers via SCP, or to a Google Cloud Storage bucket.
Results are saved as individual files in JSON new line format (.jsonl).
"""
from collections import ChainMap, OrderedDict
from collections.abc import Mapping

import logging
import os
import signal
from collections import ChainMap, OrderedDict

import configargparse
import livejson
Expand All @@ -30,8 +30,11 @@ def load_env():
It ignores variables starting with MURAKAMI_SETTINGS as those are read and
managed by configargparse."""
acc = {}
env = {k: v for k, v in os.environ.items() if k.startswith('MURAKAMI_') and
not k.startswith('MURAKAMI_SETTINGS')}
env = {
k: v
for k, v in os.environ.items()
if k.startswith("MURAKAMI_") and not k.startswith("MURAKAMI_SETTINGS")
}

def recurse(sec, value, acc):
key = sec.pop(0)
Expand All @@ -41,25 +44,30 @@ def recurse(sec, value, acc):
acc[key] = value

for k, v in env.items():
_, *sec = k.lower().split('_', maxsplit=3)
_, *sec = k.lower().split("_", maxsplit=3)
recurse(sec, v, acc)
return acc


def default_device_id():
"""Return the value of the environment variable BALENA_DEVICE_ID if set, or
an empty string."""
return os.environ.get('BALENA_DEVICE_UUID', "")
return os.environ.get("BALENA_DEVICE_UUID", "")


class TomlConfigFileParser(configargparse.ConfigFileParser):
"""
This custom parser uses Tomlkit to parse a .toml configuration file,
and then merges matching environment variables, and then puts then saves
the result while passing back just the settings portion to configargparse.
"""

def get_syntax_description(self):
"""Returns a description of the file format parsed by the class."""
msg = ("Parses a TOML-format configuration file "
"(see https://github.com/toml-lang/toml for the spec).")
msg = (
"Parses a TOML-format configuration file "
"(see https://github.com/toml-lang/toml for the spec)."
)
return msg

def parse(self, stream):
Expand All @@ -86,7 +94,7 @@ def parse(self, stream):


def main():
""" The main function for Murakami."""
"""The main function for Murakami."""
parser = configargparse.ArgParser(
auto_env_var_prefix="murakami_settings_",
config_file_parser_class=TomlConfigFileParser,
Expand All @@ -106,8 +114,9 @@ def main():
"--dynamic-state",
default=defaults.DYNAMIC_FILE,
dest="dynamic",
help=
"Path to dynamic configuration store, used to override settings via Webthings (default:" + defaults.DYNAMIC_FILE + ").",
help="Path to dynamic configuration store, used to override settings via Webthings (default:"
+ defaults.DYNAMIC_FILE
+ ").",
)
parser.add(
"-p",
Expand All @@ -116,9 +125,9 @@ def main():
default=defaults.HTTP_PORT,
help="The port to listen on for incoming connections (default: 80).",
)
parser.add("-n",
"--hostname",
help="The mDNS hostname for WebThings (default: automatic).")
parser.add(
"-n", "--hostname", help="The mDNS hostname for WebThings (default: automatic)."
)
parser.add(
"-s",
"--ssl-options",
Expand Down
5 changes: 2 additions & 3 deletions murakami/defaults.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
"""
Common default values for Murakami.
"""

SSH_PORT = 22
SSH_TIMEOUT = 5
HTTP_PORT = 80
TESTS_PER_DAY = 4
EXPORT_PATH = "/var/cache/murakami"
DYNAMIC_FILE = "/var/lib/murakami/config.json"
CONFIG_FILES = [
"/etc/murakami/murakami.toml", "~/.config/murakami/murakami.toml"
]
CONFIG_FILES = ["/etc/murakami/murakami.toml", "~/.config/murakami/murakami.toml"]
4 changes: 3 additions & 1 deletion murakami/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ExporterError(Error):
name -- The name of the runner
message -- The error message
"""

def __init__(self, name, message):
super().__init__()
self.name = name
Expand All @@ -25,10 +26,11 @@ class RunnerError(Error):
name -- The name of the runner
message -- The error message
"""

def __init__(self, name, message):
super().__init__()
self.name = name
self.message = message

def __str__(self):
return self.message
Loading