Skip to content

How to add a new INI configuration parameter

Martin Belanger edited this page Aug 14, 2026 · 4 revisions

Adding 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.

Step 1 — decide the key's type and class

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).

Step 2 — add the entry to keys[] in config-ini.c

{ "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.

Step 3 — give the value a home

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=custom

In 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.

Step 4 — apply the resolved value at connect time

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.

Step 5 — public accessors

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).

Step 6 — expose it on every CLI surface that should set it

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 / the discovery.conf-line parser (src/fabrics.c), via OPT_STRING_OPTIONAL("persistent", 'p', "no|auto|force", &persistent_arg, ...).
  • nvme config create (src/config-create.c), the same option shape, feeding libnvmf_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.

Step 7 — legacy config.json migration (only if the key has a JSON-era equivalent)

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.

Step 8 — document it

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.

Step 9 — test it

  • 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_TUNABLE keys are rejected everywhere except discovery-controller sections/paths).
  • libnvme/tests/test-fabrics.c — if the value drives runtime decision logic (like persistent/EPCSD feeding dc_decide()), add unit tests for that logic directly, independent of I/O — see test_dc_decide() for the pattern.

Quick-reference checklist

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

Further reading

persistent/EPCSD landed across several PRs: PR #3698 (core INI/config support) PR #3708 (nvme config create) PR #3776 (nvme: redesign --persistent, remove --epcsd/--no-epcsd)

The last PR in particular is where STRENUM was introduced. Read those diffs for the complete, real change if this walkthrough's excerpts aren't enough context.

Clone this wiki locally