Skip to content

Commit b62678c

Browse files
Merge pull request #469 from martin-belanger/add-CLAUDE.md
claude: add CLAUDE.md file
2 parents 641218b + ec6cabd commit b62678c

1 file changed

Lines changed: 91 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
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

Comments
 (0)