nbdmux 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nbdmux-0.1.0/.github/workflows/ci-cd.yml +146 -0
- nbdmux-0.1.0/.gitignore +10 -0
- nbdmux-0.1.0/LICENSE +28 -0
- nbdmux-0.1.0/Makefile +81 -0
- nbdmux-0.1.0/PKG-INFO +121 -0
- nbdmux-0.1.0/README.md +102 -0
- nbdmux-0.1.0/deploy/Containerfile +43 -0
- nbdmux-0.1.0/deploy/compose.yml +27 -0
- nbdmux-0.1.0/deploy/envvars.example +16 -0
- nbdmux-0.1.0/pyproject.toml +45 -0
- nbdmux-0.1.0/src/nbdmux/__init__.py +23 -0
- nbdmux-0.1.0/src/nbdmux/client.py +172 -0
- nbdmux-0.1.0/src/nbdmux/server.py +615 -0
- nbdmux-0.1.0/tests/test_nbdmux.py +329 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
tags: ["v*"]
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
# Refuse to re-release a version PyPI already has. PyPI is immutable, but a
|
|
14
|
+
# retag-after-publish would rebuild the GitHub release assets from a newer
|
|
15
|
+
# commit and silently drift from the frozen wheel. (bty/withcache integrity
|
|
16
|
+
# guard, mirrored.)
|
|
17
|
+
check-not-published:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- name: Refuse if version is already on PyPI
|
|
22
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
23
|
+
env:
|
|
24
|
+
TAG: ${{ github.ref_name }}
|
|
25
|
+
run: |
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
version="${TAG#v}"
|
|
28
|
+
if curl -sfL "https://pypi.org/pypi/nbdmux/json" \
|
|
29
|
+
| python3 -c "import sys, json; sys.exit(0 if '${version}' in json.load(sys.stdin)['releases'] else 1)"; then
|
|
30
|
+
echo "::error::v${version} is already on PyPI; refusing to re-release."
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
echo "v${version} not on PyPI; proceeding."
|
|
34
|
+
|
|
35
|
+
lint:
|
|
36
|
+
needs: check-not-published
|
|
37
|
+
runs-on: ubuntu-latest
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v4
|
|
40
|
+
- uses: actions/setup-python@v5
|
|
41
|
+
with:
|
|
42
|
+
python-version: "3.12"
|
|
43
|
+
- run: pip install ruff
|
|
44
|
+
- run: make lint format-check
|
|
45
|
+
|
|
46
|
+
test:
|
|
47
|
+
needs: check-not-published
|
|
48
|
+
strategy:
|
|
49
|
+
matrix:
|
|
50
|
+
os: [ubuntu-latest]
|
|
51
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
52
|
+
runs-on: ${{ matrix.os }}
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- uses: actions/setup-python@v5
|
|
56
|
+
with:
|
|
57
|
+
python-version: ${{ matrix.python }}
|
|
58
|
+
# Real nbd-server isn't spawned in unit tests (handlers use a fake)
|
|
59
|
+
# so we don't install it on the runner. The deploy/Containerfile is
|
|
60
|
+
# what ships it on the operator's host.
|
|
61
|
+
- run: make test
|
|
62
|
+
|
|
63
|
+
wheels:
|
|
64
|
+
needs: [lint, test]
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v4
|
|
68
|
+
- uses: actions/setup-python@v5
|
|
69
|
+
with:
|
|
70
|
+
python-version: "3.12"
|
|
71
|
+
- run: pip install build
|
|
72
|
+
- run: make wheel
|
|
73
|
+
- uses: actions/upload-artifact@v4
|
|
74
|
+
with:
|
|
75
|
+
name: dist
|
|
76
|
+
path: dist/
|
|
77
|
+
|
|
78
|
+
tag-release:
|
|
79
|
+
needs: [lint, test, wheels]
|
|
80
|
+
if: github.ref == 'refs/heads/main'
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
permissions:
|
|
83
|
+
contents: write
|
|
84
|
+
steps:
|
|
85
|
+
- uses: actions/checkout@v4
|
|
86
|
+
with:
|
|
87
|
+
fetch-depth: 0
|
|
88
|
+
token: ${{ secrets.RELEASE_PAT }}
|
|
89
|
+
- name: Tag the version if it is new
|
|
90
|
+
run: |
|
|
91
|
+
set -euo pipefail
|
|
92
|
+
version=$(sed -n 's/^__version__ = "\(.*\)"/\1/p' src/nbdmux/__init__.py)
|
|
93
|
+
test -n "$version" || { echo "could not read __version__"; exit 1; }
|
|
94
|
+
tag="v${version}"
|
|
95
|
+
if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
|
|
96
|
+
echo "${tag} already exists; nothing to do."
|
|
97
|
+
exit 0
|
|
98
|
+
fi
|
|
99
|
+
git tag "${tag}"
|
|
100
|
+
git push origin "${tag}"
|
|
101
|
+
echo "Tagged ${tag}; ci-cd.yml will publish it."
|
|
102
|
+
|
|
103
|
+
publish-pypi:
|
|
104
|
+
needs: [lint, test, wheels]
|
|
105
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
106
|
+
runs-on: ubuntu-latest
|
|
107
|
+
environment: pypi
|
|
108
|
+
permissions:
|
|
109
|
+
id-token: write
|
|
110
|
+
steps:
|
|
111
|
+
- uses: actions/checkout@v4
|
|
112
|
+
- uses: actions/setup-python@v5
|
|
113
|
+
with:
|
|
114
|
+
python-version: "3.12"
|
|
115
|
+
- run: pip install build
|
|
116
|
+
- run: make wheel
|
|
117
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
118
|
+
with:
|
|
119
|
+
packages-dir: dist/
|
|
120
|
+
|
|
121
|
+
publish-image:
|
|
122
|
+
needs: [lint, test]
|
|
123
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
124
|
+
runs-on: ubuntu-latest
|
|
125
|
+
permissions:
|
|
126
|
+
packages: write
|
|
127
|
+
contents: read
|
|
128
|
+
steps:
|
|
129
|
+
- uses: actions/checkout@v4
|
|
130
|
+
- uses: docker/setup-qemu-action@v3
|
|
131
|
+
- uses: docker/setup-buildx-action@v3
|
|
132
|
+
- uses: docker/login-action@v3
|
|
133
|
+
with:
|
|
134
|
+
registry: ghcr.io
|
|
135
|
+
username: ${{ github.actor }}
|
|
136
|
+
password: ${{ secrets.GITHUB_TOKEN }}
|
|
137
|
+
- name: Build and push multi-arch image
|
|
138
|
+
uses: docker/build-push-action@v6
|
|
139
|
+
with:
|
|
140
|
+
context: .
|
|
141
|
+
file: deploy/Containerfile
|
|
142
|
+
platforms: linux/amd64,linux/arm64
|
|
143
|
+
push: true
|
|
144
|
+
tags: |
|
|
145
|
+
ghcr.io/safl/nbdmux:latest
|
|
146
|
+
ghcr.io/safl/nbdmux:${{ github.ref_name }}
|
nbdmux-0.1.0/.gitignore
ADDED
nbdmux-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Simon A. F. Lund
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
nbdmux-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# nbdmux -- common tasks (and the home of the CI logic: the GitHub workflows
|
|
2
|
+
# call these targets, so everything CI does is reproducible locally).
|
|
3
|
+
# Run `make` for the list. Override vars on the CLI, e.g.
|
|
4
|
+
# make serve PORT=4040 make bump VERSION=0.2.0
|
|
5
|
+
PYTHON ?= python3
|
|
6
|
+
RUFF ?= ruff
|
|
7
|
+
PRECOMMIT ?= pre-commit
|
|
8
|
+
PORT ?= 4040
|
|
9
|
+
NBD_PORT ?= 10809
|
|
10
|
+
# Containerized deploy: prefer podman, fall back to docker.
|
|
11
|
+
COMPOSE ?= $(shell command -v podman >/dev/null 2>&1 && echo podman || echo docker) compose
|
|
12
|
+
COMPOSE_FILE = deploy/compose.yml
|
|
13
|
+
|
|
14
|
+
# Single source of truth = src/nbdmux/__init__.py; pyproject derives it via Hatch.
|
|
15
|
+
SRC_VERSION = $(shell sed -n 's/^__version__ = "\(.*\)"/\1/p' src/nbdmux/__init__.py)
|
|
16
|
+
|
|
17
|
+
.DEFAULT_GOAL := help
|
|
18
|
+
.PHONY: help dev hooks-install hooks lint format format-check test \
|
|
19
|
+
wheel serve up down logs version bump check clean
|
|
20
|
+
|
|
21
|
+
help: ## Show this help
|
|
22
|
+
@grep -hE '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) \
|
|
23
|
+
| awk 'BEGIN{FS=":.*?## "}{printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}'
|
|
24
|
+
|
|
25
|
+
# -- dev setup -------------------------------------------------------------
|
|
26
|
+
dev: ## Install dev tooling (ruff, build, pre-commit)
|
|
27
|
+
$(PYTHON) -m pip install --upgrade ruff build pre-commit
|
|
28
|
+
|
|
29
|
+
hooks-install: ## Install the git pre-commit hook
|
|
30
|
+
$(PRECOMMIT) install
|
|
31
|
+
|
|
32
|
+
hooks: ## Run all pre-commit hooks over the tree
|
|
33
|
+
$(PRECOMMIT) run --all-files
|
|
34
|
+
|
|
35
|
+
# -- lint / test (CI: lint job, test job) ----------------------------------
|
|
36
|
+
lint: ## Lint with ruff
|
|
37
|
+
$(RUFF) check .
|
|
38
|
+
|
|
39
|
+
format: ## Auto-format with ruff
|
|
40
|
+
$(RUFF) format .
|
|
41
|
+
|
|
42
|
+
format-check: ## Check formatting (no changes)
|
|
43
|
+
$(RUFF) format --check .
|
|
44
|
+
|
|
45
|
+
test: ## Run the test suite
|
|
46
|
+
$(PYTHON) -m unittest discover -s tests -v
|
|
47
|
+
|
|
48
|
+
# -- wheels / sdist --------------------------------------------------------
|
|
49
|
+
wheel: ## Build sdist + pure wheel
|
|
50
|
+
$(PYTHON) -m build
|
|
51
|
+
|
|
52
|
+
# -- run -------------------------------------------------------------------
|
|
53
|
+
serve: ## Run nbdmux locally (set NBDMUX_ADMIN_PASSWORD to gate the UI)
|
|
54
|
+
PYTHONPATH=src $(PYTHON) -m nbdmux.server --data-dir ./data --port $(PORT) --nbd-port $(NBD_PORT)
|
|
55
|
+
|
|
56
|
+
# -- deploy (containerized via compose) ------------------------------------
|
|
57
|
+
up: ## Bring up the containerized nbdmux
|
|
58
|
+
$(COMPOSE) -f $(COMPOSE_FILE) up -d --build
|
|
59
|
+
@echo "nbdmux up -> operator UI: http://localhost:$(PORT)/ NBD: tcp://localhost:$(NBD_PORT)"
|
|
60
|
+
|
|
61
|
+
down: ## Stop and remove the nbdmux container
|
|
62
|
+
$(COMPOSE) -f $(COMPOSE_FILE) down
|
|
63
|
+
|
|
64
|
+
logs: ## Follow the nbdmux logs
|
|
65
|
+
$(COMPOSE) -f $(COMPOSE_FILE) logs -f
|
|
66
|
+
|
|
67
|
+
# -- version (single source: src/nbdmux/__init__.py) -----------------------
|
|
68
|
+
version: ## Show the version
|
|
69
|
+
@echo "src/nbdmux/__init__.py: $(SRC_VERSION)"
|
|
70
|
+
|
|
71
|
+
bump: ## Bump the version (usage: make bump VERSION=0.2.0)
|
|
72
|
+
@test -n "$(VERSION)" || { echo "usage: make bump VERSION=X.Y.Z"; exit 2; }
|
|
73
|
+
sed -i 's/^__version__ = ".*"/__version__ = "$(VERSION)"/' src/nbdmux/__init__.py
|
|
74
|
+
@$(MAKE) --no-print-directory version
|
|
75
|
+
|
|
76
|
+
# -- aggregate / cleanup ---------------------------------------------------
|
|
77
|
+
check: lint format-check test ## Everything CI checks, locally
|
|
78
|
+
|
|
79
|
+
clean: ## Remove build/test artifacts
|
|
80
|
+
rm -rf dist build *.egg-info .pytest_cache
|
|
81
|
+
find . -type d -name __pycache__ -prune -exec rm -rf {} +
|
nbdmux-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nbdmux
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: HTTP-controlled NBD-export multiplexer for a small lab (boot Linux images over the network with overlayfs+tmpfs writes)
|
|
5
|
+
Project-URL: Homepage, https://github.com/safl/nbdmux
|
|
6
|
+
Author-email: "Simon A. F. Lund" <safl@safl.dk>
|
|
7
|
+
License: BSD-3-Clause
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: lab,nbd,netboot,overlayfs,ramboot
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: System Administrators
|
|
13
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: System :: Boot
|
|
16
|
+
Classifier: Topic :: System :: Filesystems
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# nbdmux
|
|
21
|
+
|
|
22
|
+
HTTP-controlled NBD-export multiplexer for a small lab. Register local
|
|
23
|
+
disk-image files as named NBD exports over an HTTP control plane; nbdmux
|
|
24
|
+
keeps an `nbd-server` subprocess alive that serves all registered
|
|
25
|
+
exports on a single TCP port. Targets `nbd-client` against that port
|
|
26
|
+
from an initramfs and boot the image with overlayfs over tmpfs for
|
|
27
|
+
writes (see [bty][bty]'s `ramboot` boot mode for the canonical consumer).
|
|
28
|
+
|
|
29
|
+
Designed as a peer to [withcache][withcache]: small lab, single sidecar
|
|
30
|
+
container, no third-party Python deps. Operationally:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
[ bty-web ] --HTTP--> [ nbdmux ] --supervises--> [ nbd-server ]
|
|
34
|
+
| |
|
|
35
|
+
| TCP 10809
|
|
36
|
+
| |
|
|
37
|
+
v v
|
|
38
|
+
SQLite state [ target's
|
|
39
|
+
(exports table) nbd-client ]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Components
|
|
43
|
+
|
|
44
|
+
| Path | What it is |
|
|
45
|
+
|----------------------------|-------------------------------------------------------------------------|
|
|
46
|
+
| `src/nbdmux/server.py` | The daemon. HTTP control plane + nbd-server subprocess management + operator UI |
|
|
47
|
+
| `src/nbdmux/client.py` | Stdlib-only Python client library for other tools |
|
|
48
|
+
| `deploy/Containerfile` | Single-image deploy (Python + nbd-server) |
|
|
49
|
+
| `deploy/compose.yml` | Reference compose stack |
|
|
50
|
+
|
|
51
|
+
## System dependency
|
|
52
|
+
|
|
53
|
+
nbdmux runs `nbd-server` (from the classical `nbd` project) as a
|
|
54
|
+
subprocess. Install at the OS level:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
# Debian / Ubuntu
|
|
58
|
+
sudo apt install nbd-server
|
|
59
|
+
|
|
60
|
+
# Fedora
|
|
61
|
+
sudo dnf install nbd
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The container deploy bundles it. Also make sure the `nbd` kernel
|
|
65
|
+
module + `nbd-client` are available on the consuming Linux box (the
|
|
66
|
+
target you're booting); they're in the same `nbd` package.
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
pipx install nbdmux # or: uv tool install nbdmux
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Run the daemon (development; the container deploy is the recommended
|
|
75
|
+
production path):
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
nbdmux-server --data-dir ./data --port 4040 --nbd-port 10809
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Register an image:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
curl -X POST http://localhost:4040/exports \
|
|
85
|
+
-H 'Content-Type: application/json' \
|
|
86
|
+
-d '{"name": "debian-sysdev", "file": "/path/to/debian-sysdev.img", "readonly": true}'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Then on a target Linux box:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
modprobe nbd
|
|
93
|
+
nbd-client <nbdmux-host> 10809 -name debian-sysdev /dev/nbd0
|
|
94
|
+
fdisk -l /dev/nbd0 # the .img's partition table
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## HTTP control plane
|
|
98
|
+
|
|
99
|
+
| Method | Path | Body | Returns |
|
|
100
|
+
|--------|---------------------|-----------------------------------------------|----------------|
|
|
101
|
+
| GET | `/exports` | - | array of exports |
|
|
102
|
+
| POST | `/exports` | `{name, file, readonly?: bool}` | the new export |
|
|
103
|
+
| DELETE | `/exports/{name}` | - | 204 |
|
|
104
|
+
| GET | `/healthz` | - | `ok` |
|
|
105
|
+
| GET | `/` | - | operator dashboard |
|
|
106
|
+
|
|
107
|
+
## Auth
|
|
108
|
+
|
|
109
|
+
Single-tenant, server-signed cookie -- same pattern as withcache. Set
|
|
110
|
+
`NBDMUX_ADMIN_PASSWORD` to gate the operator UI + the HTTP control
|
|
111
|
+
plane. Unset = open with a startup warning.
|
|
112
|
+
|
|
113
|
+
The NBD port itself is unauthenticated (nbd-server's classical model);
|
|
114
|
+
LAN-only assumption, firewall is the operator's responsibility.
|
|
115
|
+
|
|
116
|
+
## License
|
|
117
|
+
|
|
118
|
+
BSD-3-Clause.
|
|
119
|
+
|
|
120
|
+
[bty]: https://github.com/safl/bty
|
|
121
|
+
[withcache]: https://github.com/safl/withcache
|
nbdmux-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# nbdmux
|
|
2
|
+
|
|
3
|
+
HTTP-controlled NBD-export multiplexer for a small lab. Register local
|
|
4
|
+
disk-image files as named NBD exports over an HTTP control plane; nbdmux
|
|
5
|
+
keeps an `nbd-server` subprocess alive that serves all registered
|
|
6
|
+
exports on a single TCP port. Targets `nbd-client` against that port
|
|
7
|
+
from an initramfs and boot the image with overlayfs over tmpfs for
|
|
8
|
+
writes (see [bty][bty]'s `ramboot` boot mode for the canonical consumer).
|
|
9
|
+
|
|
10
|
+
Designed as a peer to [withcache][withcache]: small lab, single sidecar
|
|
11
|
+
container, no third-party Python deps. Operationally:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
[ bty-web ] --HTTP--> [ nbdmux ] --supervises--> [ nbd-server ]
|
|
15
|
+
| |
|
|
16
|
+
| TCP 10809
|
|
17
|
+
| |
|
|
18
|
+
v v
|
|
19
|
+
SQLite state [ target's
|
|
20
|
+
(exports table) nbd-client ]
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Components
|
|
24
|
+
|
|
25
|
+
| Path | What it is |
|
|
26
|
+
|----------------------------|-------------------------------------------------------------------------|
|
|
27
|
+
| `src/nbdmux/server.py` | The daemon. HTTP control plane + nbd-server subprocess management + operator UI |
|
|
28
|
+
| `src/nbdmux/client.py` | Stdlib-only Python client library for other tools |
|
|
29
|
+
| `deploy/Containerfile` | Single-image deploy (Python + nbd-server) |
|
|
30
|
+
| `deploy/compose.yml` | Reference compose stack |
|
|
31
|
+
|
|
32
|
+
## System dependency
|
|
33
|
+
|
|
34
|
+
nbdmux runs `nbd-server` (from the classical `nbd` project) as a
|
|
35
|
+
subprocess. Install at the OS level:
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
# Debian / Ubuntu
|
|
39
|
+
sudo apt install nbd-server
|
|
40
|
+
|
|
41
|
+
# Fedora
|
|
42
|
+
sudo dnf install nbd
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The container deploy bundles it. Also make sure the `nbd` kernel
|
|
46
|
+
module + `nbd-client` are available on the consuming Linux box (the
|
|
47
|
+
target you're booting); they're in the same `nbd` package.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pipx install nbdmux # or: uv tool install nbdmux
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Run the daemon (development; the container deploy is the recommended
|
|
56
|
+
production path):
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
nbdmux-server --data-dir ./data --port 4040 --nbd-port 10809
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Register an image:
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
curl -X POST http://localhost:4040/exports \
|
|
66
|
+
-H 'Content-Type: application/json' \
|
|
67
|
+
-d '{"name": "debian-sysdev", "file": "/path/to/debian-sysdev.img", "readonly": true}'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Then on a target Linux box:
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
modprobe nbd
|
|
74
|
+
nbd-client <nbdmux-host> 10809 -name debian-sysdev /dev/nbd0
|
|
75
|
+
fdisk -l /dev/nbd0 # the .img's partition table
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## HTTP control plane
|
|
79
|
+
|
|
80
|
+
| Method | Path | Body | Returns |
|
|
81
|
+
|--------|---------------------|-----------------------------------------------|----------------|
|
|
82
|
+
| GET | `/exports` | - | array of exports |
|
|
83
|
+
| POST | `/exports` | `{name, file, readonly?: bool}` | the new export |
|
|
84
|
+
| DELETE | `/exports/{name}` | - | 204 |
|
|
85
|
+
| GET | `/healthz` | - | `ok` |
|
|
86
|
+
| GET | `/` | - | operator dashboard |
|
|
87
|
+
|
|
88
|
+
## Auth
|
|
89
|
+
|
|
90
|
+
Single-tenant, server-signed cookie -- same pattern as withcache. Set
|
|
91
|
+
`NBDMUX_ADMIN_PASSWORD` to gate the operator UI + the HTTP control
|
|
92
|
+
plane. Unset = open with a startup warning.
|
|
93
|
+
|
|
94
|
+
The NBD port itself is unauthenticated (nbd-server's classical model);
|
|
95
|
+
LAN-only assumption, firewall is the operator's responsibility.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
BSD-3-Clause.
|
|
100
|
+
|
|
101
|
+
[bty]: https://github.com/safl/bty
|
|
102
|
+
[withcache]: https://github.com/safl/withcache
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# nbdmux container -- bundles the daemon + nbd-server + minimal Python.
|
|
2
|
+
#
|
|
3
|
+
# Build: podman build -t ghcr.io/safl/nbdmux:dev -f deploy/Containerfile .
|
|
4
|
+
# Run: see deploy/compose.yml for the production shape.
|
|
5
|
+
#
|
|
6
|
+
# Two ports:
|
|
7
|
+
# 4040 HTTP control plane + operator UI
|
|
8
|
+
# 10809 NBD (nbd-server)
|
|
9
|
+
|
|
10
|
+
FROM debian:trixie-slim
|
|
11
|
+
|
|
12
|
+
RUN apt-get update \
|
|
13
|
+
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
14
|
+
nbd-server \
|
|
15
|
+
python3 \
|
|
16
|
+
python3-pip \
|
|
17
|
+
ca-certificates \
|
|
18
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
19
|
+
|
|
20
|
+
WORKDIR /app
|
|
21
|
+
COPY pyproject.toml README.md LICENSE /app/
|
|
22
|
+
COPY src/ /app/src/
|
|
23
|
+
RUN pip install --break-system-packages --no-cache-dir .
|
|
24
|
+
|
|
25
|
+
# Persistent state lives under /data. The compose stack binds this to a
|
|
26
|
+
# named volume so a container rebuild keeps registered exports.
|
|
27
|
+
ENV NBDMUX_DATA_DIR=/data
|
|
28
|
+
VOLUME ["/data"]
|
|
29
|
+
|
|
30
|
+
# Image files we serve as NBD exports live under /images (a read-only
|
|
31
|
+
# bind from the host that holds the actual .img bytes). This is just
|
|
32
|
+
# the convention; the daemon will serve any absolute path the operator
|
|
33
|
+
# registers.
|
|
34
|
+
VOLUME ["/images"]
|
|
35
|
+
|
|
36
|
+
EXPOSE 4040 10809
|
|
37
|
+
|
|
38
|
+
HEALTHCHECK --interval=15s --timeout=3s --start-period=5s \
|
|
39
|
+
CMD python3 -c "import urllib.request, sys; \
|
|
40
|
+
sys.exit(0 if urllib.request.urlopen('http://localhost:4040/healthz', timeout=2).status == 200 else 1)"
|
|
41
|
+
|
|
42
|
+
ENTRYPOINT ["nbdmux-server"]
|
|
43
|
+
CMD ["--data-dir", "/data", "--port", "4040", "--nbd-port", "10809"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Reference compose stack -- run as a sidecar (e.g. next to bty + withcache).
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# cp envvars.example envvars && "${EDITOR:-vi}" envvars # set NBDMUX_ADMIN_PASSWORD
|
|
5
|
+
# podman compose --env-file envvars up -d
|
|
6
|
+
# # http://<host>:4040/ (operator UI)
|
|
7
|
+
# # tcp://<host>:10809 (NBD)
|
|
8
|
+
|
|
9
|
+
services:
|
|
10
|
+
nbdmux:
|
|
11
|
+
image: ghcr.io/safl/nbdmux:latest
|
|
12
|
+
restart: unless-stopped
|
|
13
|
+
environment:
|
|
14
|
+
NBDMUX_ADMIN_PASSWORD: "${NBDMUX_ADMIN_PASSWORD:-}"
|
|
15
|
+
ports:
|
|
16
|
+
- "${HOST_HTTP_PORT:-4040}:4040"
|
|
17
|
+
- "${HOST_NBD_PORT:-10809}:10809"
|
|
18
|
+
volumes:
|
|
19
|
+
# Persistent: state.db + nbd-server.conf survive restarts here.
|
|
20
|
+
- nbdmux-data:/data
|
|
21
|
+
# Read-only bind of the host directory holding the .img files
|
|
22
|
+
# nbdmux will serve as NBD exports. Operator places files here
|
|
23
|
+
# (or, in the bty integration, bty-web decompresses into here).
|
|
24
|
+
- "${IMAGES_HOST_DIR:-./images}:/images:ro"
|
|
25
|
+
|
|
26
|
+
volumes:
|
|
27
|
+
nbdmux-data:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Copy to ``envvars`` and edit. The compose stack reads this via --env-file.
|
|
2
|
+
|
|
3
|
+
# Gate the operator UI + control-plane writes. Empty = open (logs a
|
|
4
|
+
# startup warning; only safe on a LAN-internal nbdmux).
|
|
5
|
+
NBDMUX_ADMIN_PASSWORD=
|
|
6
|
+
|
|
7
|
+
# Optional: host ports to publish. Default: same as the in-container
|
|
8
|
+
# defaults (4040 HTTP, 10809 NBD).
|
|
9
|
+
#HOST_HTTP_PORT=4040
|
|
10
|
+
#HOST_NBD_PORT=10809
|
|
11
|
+
|
|
12
|
+
# Where the .img files live on the host. Bound read-only into the
|
|
13
|
+
# container at /images. nbdmux serves whatever absolute path the
|
|
14
|
+
# operator registers via POST /exports; using a single host directory
|
|
15
|
+
# for the .img files just keeps the bind-mount simple.
|
|
16
|
+
#IMAGES_HOST_DIR=./images
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nbdmux"
|
|
7
|
+
dynamic = ["version"] # single source of truth: src/nbdmux/__init__.py:__version__
|
|
8
|
+
description = "HTTP-controlled NBD-export multiplexer for a small lab (boot Linux images over the network with overlayfs+tmpfs writes)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "BSD-3-Clause" }
|
|
12
|
+
authors = [{ name = "Simon A. F. Lund", email = "safl@safl.dk" }]
|
|
13
|
+
keywords = ["nbd", "netboot", "ramboot", "overlayfs", "lab"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: System Administrators",
|
|
18
|
+
"License :: OSI Approved :: BSD License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: System :: Boot",
|
|
21
|
+
"Topic :: System :: Filesystems",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [] # stdlib only, by design (delegates to the system's nbd-server)
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/safl/nbdmux"
|
|
27
|
+
|
|
28
|
+
[project.scripts]
|
|
29
|
+
nbdmux-server = "nbdmux.server:main"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/nbdmux"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.version]
|
|
35
|
+
path = "src/nbdmux/__init__.py"
|
|
36
|
+
|
|
37
|
+
[tool.ruff]
|
|
38
|
+
line-length = 100
|
|
39
|
+
target-version = "py310"
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "W", "I", "UP", "B"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint.per-file-ignores]
|
|
45
|
+
"tests/*" = ["E402"]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""nbdmux -- HTTP-controlled NBD-export multiplexer.
|
|
2
|
+
|
|
3
|
+
Two surfaces:
|
|
4
|
+
|
|
5
|
+
- ``nbdmux-server`` (``nbdmux.server:main``) -- the daemon. Manages an
|
|
6
|
+
``nbd-server`` subprocess that exposes registered local files as
|
|
7
|
+
named NBD exports on a TCP port (default 10809). Operator dashboard
|
|
8
|
+
+ HTTP control API on a separate port (default 4040).
|
|
9
|
+
- ``nbdmux.client`` -- a tiny stdlib-only library for other tools
|
|
10
|
+
(e.g. bty) to register / list / unregister exports without
|
|
11
|
+
reimplementing the HTTP API.
|
|
12
|
+
|
|
13
|
+
Designed for the same niche as ``withcache``: a small lab, a single
|
|
14
|
+
sidecar container, no third-party Python deps. The system-level
|
|
15
|
+
dependency is ``nbd-server`` (Debian / Ubuntu: ``apt install
|
|
16
|
+
nbd-server``; Fedora: ``dnf install nbd``).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .client import add_export, list_exports, remove_export
|
|
20
|
+
|
|
21
|
+
__version__ = "0.1.0"
|
|
22
|
+
|
|
23
|
+
__all__ = ["__version__", "add_export", "list_exports", "remove_export"]
|