|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Build and Test Commands |
| 6 | + |
| 7 | +```bash |
| 8 | +# First-time setup (or after `make purge`) |
| 9 | +meson setup .build |
| 10 | +meson compile -C .build |
| 11 | + |
| 12 | +# Full test suite (use this — it sets PYTHONPATH correctly) |
| 13 | +meson test -C .build |
| 14 | + |
| 15 | +# Run a single test file during development |
| 16 | +PYTHONPATH=".build/subprojects/nvme-cli/libnvme:.build" python3 test/test-foo.py -v |
| 17 | + |
| 18 | +# Lint and format checks |
| 19 | +ruff check .build/staslib .build/stafctl .build/stacd .build/stafctl .build/stafd .build/stasadm |
| 20 | +ruff format --check .build/staslib |
| 21 | + |
| 22 | +# Coverage integration test (requires nvmet kernel module, stops stafd/stacd first) |
| 23 | +make coverage |
| 24 | + |
| 25 | +# Make wrappers for common Meson operations |
| 26 | +make # build |
| 27 | +make test # run tests |
| 28 | +make clean # remove build artifacts, keep .build/ |
| 29 | +make purge # remove .build/ entirely |
| 30 | +``` |
| 31 | + |
| 32 | +## Critical: The `.build/` Directory |
| 33 | + |
| 34 | +Meson copies all Python source files into `.build/` at setup time. Tests run against `.build/`, not the source tree directly. After editing source files, Meson's incremental build often does **not** re-copy them (the `configure_file(copy: true)` step doesn't track file timestamps reliably). The safest approach after non-trivial edits: |
| 35 | + |
| 36 | +```bash |
| 37 | +rm -rf .build/ && meson setup .build && meson compile -C .build |
| 38 | +``` |
| 39 | + |
| 40 | +`staslib/defs.py` is a template — `@VERSION@`, `@ETC@`, etc. are substituted by Meson into `.build/staslib/defs.py`. Always read the source version for logic, but the runnable version lives in `.build/`. |
| 41 | + |
| 42 | +Top-level scripts (`stafd.py`, `stacd.py`, `stafctl.py`, etc.) are installed **without the `.py` extension** — the built copies are `.build/stafd`, `.build/stafctl`, etc. |
| 43 | + |
| 44 | +## Architecture |
| 45 | + |
| 46 | +Two cooperating systemd daemons: |
| 47 | + |
| 48 | +- **`stafd`** (STorage Appliance Finder): discovers NVMe controllers via Avahi mDNS (`_nvme-disc._tcp`) and/or manual config, connects to them, retrieves discovery log pages, and exposes results on D-Bus (`org.nvmexpress.staf`). |
| 49 | +- **`stacd`** (STorage Appliance Connector): reads discovery results from `stafd` via D-Bus, establishes NVMe-oF I/O controller connections, exposes state on D-Bus (`org.nvmexpress.stac`). |
| 50 | + |
| 51 | +Both daemons are driven by the **GLib main loop** — no `asyncio`, no threading primitives for I/O. Timers, name resolution, udev events, and D-Bus signals all funnel through `GLib.MainLoop`. |
| 52 | + |
| 53 | +### `staslib/` Module Roles |
| 54 | + |
| 55 | +| Module | Purpose | |
| 56 | +|--------|---------| |
| 57 | +| `stas.py` | Abstract base classes `ControllerABC` and `ServiceABC`; `load_idl()` for D-Bus introspection XML; `_read_lkc()` / `_write_lkc()` for pickle-based last-known-config | |
| 58 | +| `ctrl.py` | Concrete `Controller` base (wraps `libnvme.nvme.ctrl`), `Dc` (discovery controller), `Ioc` (I/O controller) | |
| 59 | +| `service.py` | `Service` subclass wiring together D-Bus, Avahi, udev, and controller lifecycle | |
| 60 | +| `conf.py` | `SvcConf`, `SysConf`, `NvmeOptions`, `NbftConf` — all **singletons** (see `singleton.py`); `OrderedMultisetDict` for repeated config keys | |
| 61 | +| `gutil.py` | GLib utilities: `GTimer` (restartable one-shot timer), `Deferred` (idle-scheduled callback), `AsyncTask` (thread-pool operation with GLib callbacks), `NameResolver` | |
| 62 | +| `avahi.py` | Avahi D-Bus client; `ServiceDiscovery` tracks `_nvme-disc._tcp` announcements; `ValueRange` implements exponential-backoff step sequences | |
| 63 | +| `trid.py` | `TID` — immutable transport ID (transport, traddr, trsvcid, subsysnqn, host-iface, host-traddr) used as dict key throughout | |
| 64 | +| `iputil.py` | IP address utilities, interface enumeration | |
| 65 | +| `udev.py` | `UDEV` singleton wrapping `pyudev`; device add/remove/change event dispatch | |
| 66 | +| `nbft.py` | NBFT (NVMe Boot Firmware Table) reader via `libnvme.nvme.nbft_get()` | |
| 67 | +| `log.py` | Logging initialisation (syslog + stderr) | |
| 68 | + |
| 69 | +### Key Design Patterns |
| 70 | + |
| 71 | +**Singletons**: `SvcConf`, `SysConf`, `NvmeOptions`, `NbftConf` use a `Singleton` metaclass. Always call them normally — e.g. `conf.SvcConf()` — Python returns the existing instance after first construction. Pass `default_conf=` only on first construction. |
| 72 | + |
| 73 | +**GTimer restart behaviour**: `GTimer.start()` on an already-running timer resets the deadline from *now* (via `set_ready_time()`). Calling it on a stopped timer creates a new GLib source. |
| 74 | + |
| 75 | +**Controller removal**: When `_on_ctrl_removed()` fires (udev "remove" event), `_connect_attempts` resets to 0 and the retry timer restarts at `FAST_CONNECT_RETRY_PERIOD_SEC` — effectively a fresh connection attempt. |
| 76 | + |
| 77 | +**Last-known-config**: Persisted as a pickle file in `$RUNTIME_DIRECTORY` (typically `/run/nvme-stas/`). `pickle.load()` is safe here because that directory is root-only writable. |
| 78 | + |
| 79 | +## Tests |
| 80 | + |
| 81 | +Test files are in `test/` and are named `test-*.py`. They use `unittest` (not pytest, despite pytest being listed as a dev dependency — Meson runs them directly with `python3`). |
| 82 | + |
| 83 | +Tests that need filesystem mocking use `pyfakefs` (`from pyfakefs.fake_filesystem_unittest import TestCase`). |
| 84 | + |
| 85 | +`test-stasadm.py` loads `stasadm.py` at module level using `importlib.util.spec_from_file_location` with `sys.argv`, `sys.exit`, `sys.stdout`, and `sys.stderr` all mocked — because `stasadm.py` runs entry-point code at import time. |
| 86 | + |
| 87 | +The Avahi test requires `avahi-daemon` and `dbus` to be active; Meson skips it automatically if they are not running. |
| 88 | + |
| 89 | +## Python Version Compatibility |
| 90 | + |
| 91 | +Code must remain compatible with **Python 3.6+**. This is enforced by `vermin` (configured in `test/vermin.conf`, target `3.6`). Excluded from vermin analysis: `importlib.resources` and `importlib_resources` (handled by the three-tier fallback in `stas.py`). Line length limit is **120** characters (configured in `pyproject.toml`). |
0 commit comments