-
Notifications
You must be signed in to change notification settings - Fork 729
How to add a new INI configuration parameter
This is a walkthrough of everything touched when adding a new key to the NVMe-oF INI configuration format (nvme-fabrics.conf and its drop-ins). The format itself is documented in libnvme/design/CONFIG.md; this page is about the mechanics of extending it, not the format's own semantics.
The worked example throughout is persistent — the discovery-controller persistence-mode key (no/auto/force), added alongside EPCSD support. It's a good example precisely because it needed a new kind of value (a fixed set of string enum values, not a plain int/bool/string), so it touches every step below, including the ones a simpler key would skip. Where a step is STRENUM-specific, that's called out — most new keys are a plain INT/BOOL/STRING and only need steps 1, 2, 4, 6, 8, and 9.
Every key in libnvme/src/nvme/config-ini.c's keys[] table has two classifications:
Type (enum libnvmf_key_type, in config-ini.h) drives value validation:
| Type | Usage |
|---|---|
LIBNVMF_KEY_STRING |
any string |
LIBNVMF_KEY_INT |
parsed with strtol(), base 0. |
LIBNVMF_KEY_BOOL |
parsed with shr_parse_bool(). |
LIBNVMF_KEY_STRENUM |
a string decoding to one of a fixed set of values. Needs a strenum_validate callback (step 2b below). persistent is the only key using this type today. |
Class (enum libnvmf_key_class, in config-ini.h) drives where the key may legally appear:
| Class | Usage |
|---|---|
LIBNVMF_KEY_TUNABLE |
any section, per-path override allowed on a controller = line. Most connection tunables (nr-io-queues, keep-alive-tmo, tls, ...) are this class, split between plain tunables and LIBNVMF_KEY_SECURITY for anything crypto-related. |
LIBNVMF_KEY_SECURITY |
any section, but never on a controller = line (security parameters are bound to (hostnqn, subsysnqn), not a path). |
LIBNVMF_KEY_IDENTITY |
[Host] only: hostnqn, hostid, hostsymname. |
LIBNVMF_KEY_NQN / LIBNVMF_KEY_CONTROLLER
|
endpoint sections only: [Discovery Controller] or [Subsystem]
|
LIBNVMF_KEY_DC_TUNABLE |
discovery-controller sections/paths only. persistent is this class: it's meaningless for an I/O controller entry, so the parser rejects it there. |
For persistent: type LIBNVMF_KEY_STRENUM (three fixed values), class LIBNVMF_KEY_DC_TUNABLE (discovery controllers only).
{ "persistent", LIBNVMF_KEY_STRENUM, LIBNVMF_KEY_DC_TUNABLE, check_persistent },2a. For STRING/INT/BOOL keys the fourth field (strenum_validate) is unused — leave it off the initializer, matching every non-STRENUM row in the table.
2b. STRENUM-specific: write a small check_<name>() function and point strenum_validate at it:
static int check_persistent(const char *value)
{
enum libnvmf_persistent p;
return _libnvmf_persistent_from_str(value, &p);
}The switch in libnvmf_key_check_value() (also in config-ini.c) does not change — it already calls whatever strenum_validate points to for any LIBNVMF_KEY_STRENUM key. Adding a second STRENUM key in the future is just another check_<name>() function; no shared code needs touching.
Most tunables already have a field on struct libnvme_fabrics_config (nr_io_queues, keep_alive_tmo, ...) — a brand-new plain tunable just adds a field there.
STRENUM-specific: a value that needs enum-string translation at the API boundary doesn't live as a raw struct field the way a plain int/bool does. persistent lives as enum libnvmf_persistent persistent on struct libnvmf_context (private-fabrics.h), annotated:
enum libnvmf_persistent persistent; // !access:read=custom,write=customIn the case of persistent, we wanted to keep the enum definition private — callers should only ever see the "no"/"auto"/"force" strings, never the underlying enum values. The !access:read=custom,write=custom annotation is what makes that possible: it opts the field out of the generator entirely, in favor of hand-written accessors that do the enum↔string translation instead of a generated getter/setter that would just expose the raw enum. That translation itself is _libnvmf_persistent_from_str() / a matching _to_str(), in fabrics.c.
For a plain INT/BOOL/STRING key, skip this translation layer entirely — the value goes straight into its struct libnvme_fabrics_config field with no custom accessor needed.
Once the INI resolver has merged precedence (drop-ins over the main file, per-path over section defaults, exactly like keep-alive-tmo), each key in the final struct libnvmf_params store is applied by apply_param() in libnvme/src/nvme/config.c — a string-keyed if/else chain, one branch per key:
} else if (!strcmp(key, "persistent")) {
libnvmf_context_set_persistent(fctx, value);
}A plain tunable's branch just writes the struct field directly (cfg->nr_io_queues = strtol(value, NULL, 0)); persistent's branch delegates to its own accessor instead, since that's where the string→enum translation happens.
Note the reset-form guard at the top of apply_param(): an explicit empty value (key =) is deliberately left unapplied for every key, not just persistent — nothing new to do here for a new key, it falls out of the existing early return.
STRENUM-specific: because persistent opted out of the generator in step 3, it needs hand-written, kdoc'd, __shr_public accessors — libnvmf_context_set_persistent() / libnvmf_context_get_persistent() in fabrics.c. A plain tunable gets this for free from the accessor generator and this step is a no-op — see accessor-workflow.md for the regeneration command (meson compile -C .build update-fabrics-accessors).
Every place a user can build or modify a connection needs the option wired in, funneling into libnvmf_params_set() — the same store the INI parser itself produces, so validation goes through the exact same check_persistent() path either way, no matter whether the value came from a config file or a command line.
persistent is wired into two places:
-
nvme connect/ thediscovery.conf-line parser (src/fabrics.c), viaOPT_STRING_OPTIONAL("persistent", 'p', "no|auto|force", &persistent_arg, ...). -
nvme config create(src/config-create.c), the same option shape, feedinglibnvmf_params_set(params, "persistent", persistent)before the entry is emitted to the INI file.
OPT_STRING_OPTIONAL (value can be attached with = or omitted for a default) is what persistent uses because auto is the sensible bare-flag default; a key without a sensible default value would just use OPT_STRING, OPT_INT, or OPT_FLAG (for BOOL) instead.
Skip this step entirely for a parameter with no history before the INI format. persistent has one: the old JSON format's boolean "persistent" field. config-convert.c's apply_dc_persistent() maps it forward:
/*
* The legacy config.json format predates EPCSD; its boolean "persistent"
* meant unconditional persistence. Map true to "force", not the new
* best-effort "auto" default, so migrating an existing config.json doesn't
* silently change behavior for a connection that was persistent before.
*/The comment is the important part of this step, not the code: a migration mapping is a semantic decision (what did the old value actually mean, and which new value preserves that behavior), and that reasoning needs to be written down at the call site, not left implicit.
Man pages are the per-key reference and always need updating. Every CLI surface from step 6 needs its OPT_STRING_OPTIONAL/etc. description kept in sync with its actual behavior. Documentation/nvme-connect.txt, Documentation/nvme-discover.txt, and Documentation/nvme-config-create.txt all already document --persistent — use their wording as the template for a new key's option description, including documented quirks (e.g. --persistent's own documented gotcha: a value given as a separate argv token instead of glued with = is silently dropped, not an error — that's an argconfig/OPT_STRING_OPTIONAL property worth calling out for any new optional-value option, not just this one).
CONFIG.md is deliberately not a per-key reference — don't add one. It documents the format's mechanics (sections, precedence, drop-ins, ...) using a handful of realistic examples, not an exhaustive list of every key. keys[] in config-ini.c is already that list, and it's the one place guaranteed to stay accurate — a hand-maintained table duplicating it would drift the moment a key is added without the table being updated in lockstep, which in practice doesn't happen. Only touch CONFIG.md for a new key if it introduces or changes a mechanic the doc needs to explain (a new section type, a new precedence wrinkle, a value form worth illustrating in one of the example files) — not simply because the key exists.
-
libnvme/tests/config-ini.c— parser-level coverage, both positive and negative:- A positive case confirming the key parses and round-trips through
libnvmf_params_get()(see the"persistent = auto\n"case). - A negative case per class restriction the key is supposed to enforce (see the
"persistent in [I/O Controller Defaults]"/"persistent in [Subsystem]"rejection cases —LIBNVMF_KEY_DC_TUNABLEkeys are rejected everywhere except discovery-controller sections/paths).
- A positive case confirming the key parses and round-trips through
-
libnvme/tests/test-fabrics.c— if the value drives runtime decision logic (likepersistent/EPCSD feedingdc_decide()), add unit tests for that logic directly, independent of I/O — seetest_dc_decide()for the pattern.
| Step | File | Skip if... |
|---|---|---|
| 1 | (design decision, no file) | never |
| 2 | libnvme/src/nvme/config-ini.c |
never |
| 2b |
libnvme/src/nvme/config-ini.c (+ config-ini.h type comment) |
not STRENUM |
| 3 |
libnvme/src/nvme/private-fabrics.h (or wherever the owning struct lives) |
rare — plain keys reuse an existing field |
| 4 |
libnvme/src/nvme/config.c (apply_param()) |
never |
| 5 |
libnvme/src/nvme/fabrics.c (or accessor regeneration) |
field uses generated accessors |
| 6 | every CLI command that should set it (src/fabrics.c, src/config-create.c, ...) |
key is INI-only, never CLI-settable |
| 7 | src/config-convert.c |
no legacy config.json equivalent |
| 8 | relevant Documentation/*.txt (always); libnvme/design/CONFIG.md (only for a new mechanic, not just a new key) |
man pages: never. CONFIG.md: key introduces no new mechanic |
| 9 |
libnvme/tests/config-ini.c (+ test-fabrics.c if runtime logic) |
never |
persistent/EPCSD landed across PR #3698 (core INI/config support) and PR #3708 (nvme config create) — read those diffs for the complete, real change if this walkthrough's excerpts aren't enough context.