libnvme/config: add an INI-format NVMe-oF config parser#3566
libnvme/config: add an INI-format NVMe-oF config parser#3566martin-belanger wants to merge 8 commits into
Conversation
Describe the INI-format connection configuration intended to replace /etc/nvme/config.json: the file layout (a main nvme-fabrics.conf plus an optional nvme-fabrics.conf.d/ drop-in directory), the single- and multi-personality models, the precedence/merge rules with file-scoped defaults, the TLS and DH-CHAP security parameters, and the validation tiers. The format is parsed by libnvme so that nvme-cli, nvme-discoverd, and nvme-stas can all share one dependency-free parser. libnvme itself never reads config.json: conversion is a one-shot, nvme-cli-side job (nvme config convert), not a runtime fallback -- matching the "no third-party dependency" rationale this document already gives for choosing INI in the first place. Signed-off-by: Martin Belanger <martin.belanger@dell.com>
Tokenize [Section] / "key = value" / # comment files and report events
through a callback; all meaning stays with the consumer. An empty
value ("key =") is distinct from an absent key, and a malformed
section header clears the current section so the lines below it are
never misattributed.
Signed-off-by: Martin Belanger <martin.belanger@dell.com>
The bag keeps each connection parameter in one of three states -- unset (inherit), "" (reset to the kernel default), or a value -- which is what makes the precedence cascade expressible. The key table declares each recognized key once: its value type and the class that says where it may appear (tunable, security, [Host] identity, endpoint). The legacy numeric --tls_key is deliberately absent, and fast-io-fail-tmo uses the hyphenated spelling the 3.0 rename settles on. Signed-off-by: Martin Belanger <martin.belanger@dell.com>
Record one file's sections and lines faithfully; nothing is resolved here (the cascade resolver consumes a list of these). [Global] and the per-type defaults merge with a warning when repeated, [Host] is a hard singleton, and endpoint sections are repeatable. controller= lines split into the path's raw addressing strings and the per-path tunable overrides; unknown or security keys on a path line are errors. Tier 1 problems fail the file with a file:line diagnostic, Tier 2 warns and continues. Addressing stays raw strings, not a TID: a config file is a human interface and traddr/host-traddr may legitimately name a host (libnvme/design/INTEGRATION.md's worked example), while TID construction rejects hostnames (libnvme/design/TID.md) -- resolving them is the consumer's job, not the parser's. Signed-off-by: Martin Belanger <martin.belanger@dell.com>
Load the main file plus its sorted drop-ins and apply the precedence cascade; the result is what consumers see: a flat list of (role, addressing, identity, params) connections, main file first. Each resolved DC carries its file scope's DC- and IOC-default parameter blocks, so controllers *discovered* via that DC (referral DCs, DLP entries) inherit the right defaults; mDNS-found DCs draw from the top-level scope. The relational Tier 1 rules reject a nameless drop-in persona, one hostid shared by two personas, and one hostnqn carrying two hostids. Addressing and identity stay raw strings, not a TID: traddr may still be a hostname at this point (the parser never resolves it), and TID construction rejects one -- building it is the consumer's job, after it resolves. Signed-off-by: Martin Belanger <martin.belanger@dell.com>
Give the resolved model its public face: read/validate/free on the configuration, iteration over the flat connection list, per-connection getters, and the discovered-controller parameter lookup a discovery daemon needs to parameterize controllers it finds at runtime (DLP, AEN, mDNS) that have no section in any file. Files, sections and drop-ins remain parser-internal; the only things that cross the API boundary are (role, addressing, identity, params) tuples. Addressing and identity are per-field getters, not a TID: traddr/host-traddr may still be a hostname at this point, and TID construction rejects one -- building it is the caller's job, once it has resolved. The parameter bag's read half becomes public with it, since connection parameters are handed out as borrowed bags. The new test links against the real shared library rather than the test library, so it also proves the version-script exports. Signed-off-by: Martin Belanger <martin.belanger@dell.com>
The whole point of the flat (role, addressing, identity, params) list is that each element can become the option list of one "nvme connect" invocation -- a discovery daemon's transient units exec exactly that, and a resolved "config show" can render each connection as a copy-pasteable command line. Emit the TID's addressing and identity first, then the parameters; unset and reset parameters are not emitted so the kernel default applies, and boolean parameters become bare flags. Taking the (TID, params) pair rather than a connection lets the same emitter serve runtime-discovered controllers, whose TID the consumer builds itself and whose parameters come from libnvmf_config_resolve_discovered(). It's also why the pair, not the connection, is the argument: a connection's addressing is raw strings (config.h), so the caller resolves and builds the TID before calling in, exactly as the test now does. Signed-off-by: Martin Belanger <martin.belanger@dell.com>
A copy-paste error left host_iface sourced from trsvcid in the fctx built for libnvmf_ctrl_find(), so an interface-scoped lookup against an existing subsystem never matched on host_iface. Signed-off-by: Martin Belanger <martin.belanger@dell.com>
|
|
||
| [Host] | ||
| # hostnqn comes from /etc/nvme/hostnqn by default | ||
| # hostid comes from /etc/nvme/hostid by default |
There was a problem hiding this comment.
I wonder if we should not move them also into the global section. WDYT?
though I haven't figured out what the impact of this would be... if those stay separate, it would be good to document it here why this is the case.
| hostnqn = nqn.2014-08.org.nvmexpress:uuid:1111…-A | ||
| hostid = 46ba5037-7ce5-41fa-9452-48477bf00080 | ||
| hostsymname = Hydra | ||
| dhchap-secret = DHHC-1:00:… |
There was a problem hiding this comment.
Maybe remove the dhcap example here, we should not encourage people to use this method for loading the keys into the kernel. Another big TODO on the list is to make the secret loading a bit more useful for production systems.
Or just rename the example testing-fabrics.conf :)
|
|
||
| ### Security parameters | ||
|
|
||
| A connection's security settings fall into two families — **authentication** (DH-CHAP, which proves the host and controller identities in-band) and **encryption** (TLS, which protects the data on the wire) — plus one parameter that bridges them. Because they are bound to the host+subsystem relationship, they live on the **endpoint section**, not on a `controller =` path (the exception above). |
There was a problem hiding this comment.
Maybe add something adding secrets to the config file are there for testing purposes. It's not a good idea to do it in a production system. The idea is that the keys are handled by a 3rd party which loads them into the kernel keyring. I was looking into how this works with the systemd ecosystem. Unfortunately, never got anywhere. That's why currently we have this very simple udev rule which loads the file content in /etc/nvme/keys into the kernel. Not really good either, it's stop gap solution.
| * The tokenizing loop, over a buffer the caller already owns and may | ||
| * modify in place. libnvmf_ini_parse_buf() dups its const input once to | ||
| * get one of these; libnvmf_ini_parse_file() already allocated its own and | ||
| * passes it straight through, with no second copy. |
There was a problem hiding this comment.
I'd say the second part should be removed, that is just reasoning not really helping the reader to understand this function.
|
|
||
| /* ...unless unterminated, empty, or followed by text. */ | ||
| if (!end || end == libnvmf_trim(s + 1) || | ||
| *libnvmf_trim(end + 1)) { |
There was a problem hiding this comment.
running libnvmf_trim twice is a bit excessive.
| if (!ctx || !text || !callback) | ||
| return -EINVAL; | ||
|
|
||
| copy = xstrdup(text); |
There was a problem hiding this comment.
on line 128 we test for !text so a simple strdup should be fine or drop the check in line 128
| { | ||
| __cleanup_free char *text = NULL; | ||
| struct stat st; | ||
| FILE *f; |
There was a problem hiding this comment.
__cleanup_file FILE *f = NULL would avoid those explicit fclose in the error path
| return -EINVAL; | ||
|
|
||
| return parse_lines(text, callback, user_data); | ||
| } |
There was a problem hiding this comment.
Nice little parser. I like :)
| { | ||
| int *count = user_data; | ||
|
|
||
| if (++(*count) == 2) |
There was a problem hiding this comment.
This is not that easy to read (remembering the operator preference rules...), maybe make this a bit explicit.
| struct libnvmf_params { | ||
| struct kv *head; | ||
| struct kv **tail; | ||
| }; |
There was a problem hiding this comment.
fwiw, there is also a ccan list implementation, would avoid another open coded version...
| } | ||
| e->value = copy; | ||
| *p->tail = e; | ||
| p->tail = &e->next; |
There was a problem hiding this comment.
this part could go into a smaller function something like libnvmf_params_new_add (a good name obviously is needed), would document what this snippet does.
| } | ||
|
|
||
| int libnvmf_params_merge(struct libnvmf_params *dst, | ||
| const struct libnvmf_params *src) |
There was a problem hiding this comment.
is the order of the arguments sane? I usually prefer it to have it src, dst but that's me. If there are other interfaces which follow this pattern, than okay, but if not, I'd rather have the other way around.
| long v; | ||
|
|
||
| errno = 0; | ||
| v = strtol(value, &end, 10); |
There was a problem hiding this comment.
could be also support hex values?
| return -EINVAL; | ||
|
|
||
| copy = strdup(value); | ||
| copy = xstrdup(value); |
There was a problem hiding this comment.
value is NULL checked, so strdup works just fine.
| if (!e) | ||
| goto fail; | ||
| e->key = strdup(key); | ||
| e->key = xstrdup(key); |
There was a problem hiding this comment.
same here key is NULL checked.
| { "nqn", LIBNVMF_KEY_STRING, LIBNVMF_KEY_NQN }, | ||
| { "controller", LIBNVMF_KEY_STRING, LIBNVMF_KEY_CONTROLLER }, | ||
| { "nqn", LIBNVMF_KEY_STRING, LIBNVMF_KEY_NQN }, | ||
| { "controller", LIBNVMF_KEY_STRING, LIBNVMF_KEY_CONTROLLER } |
There was a problem hiding this comment.
these whitespace changes are not really necessary. could all go into the previous patch.
| struct libnvmf_params *dc_defaults; | ||
| struct libnvmf_params *ioc_defaults; /* [I/O Controller Defaults] */ | ||
| bool has_host; /* file carries a [Host] section */ | ||
| char *hostnqn; /* "" records an explicit reset */ |
There was a problem hiding this comment.
this comment is redundant, no? I though this is the model for all parameters.
| char *traddr; | ||
| char *trsvcid; /* NULL when the file left it unset */ | ||
| char *host_traddr; /* NULL when the file left it unset */ | ||
| char *host_iface; /* NULL when the file left it unset */ |
There was a problem hiding this comment.
do we need those comments, I find this obvious.
| * raw strings, not a TID: a config file is a human interface and may | ||
| * legitimately name a host, and TID construction rejects hostnames | ||
| * (libnvme/design/TID.md) -- resolving them is the consumer's job. | ||
| * subsysnqn and the host identity are filled in at resolve time. |
There was a problem hiding this comment.
The first part of the documntation is good, the reasoning why is already lenghty explained in the documents.
| char *host_iface; /* NULL when the file left it unset */ | ||
| /* per-path tunables; never security */ | ||
| struct libnvmf_params *overrides; | ||
| unsigned int line; /* 1-based config-file line, for diagnostics */ |
There was a problem hiding this comment.
the comment doesn't really help me at all, need to look at the code. maybe just say used 'for diagnostics'?
|
|
||
| struct libnvme_global_ctx; | ||
|
|
||
| struct libnvmf_conf_path { |
There was a problem hiding this comment.
The main issue I see with this interface is, that it's makes this part of the API fixed. We can't really change it after the release without breaking existing users. Do we really want to do this?
There was a problem hiding this comment.
unless this header is not exposed as API... :)
| return -ENOMEM; | ||
| } | ||
|
|
||
| for (tail = &pc->f->endpoints; *tail; tail = &(*tail)->next) |
There was a problem hiding this comment.
if we move the structs into the implementation, we just could use the ccan list APIs...
There was a problem hiding this comment.
and after reading through all (and remembering the cover letter...) I'd say it makes sense to use the ccan lists which also give some level of type information.
| pc->ep = NULL; | ||
|
|
||
| if (!strcmp(name, "Global")) { | ||
| if (pc->f->global) |
There was a problem hiding this comment.
struct libnvmf_conf_file *f = pc->f;
would make it a bit simpler to read IMO.
| { "hdr-digest", LIBNVMF_KEY_BOOL, LIBNVMF_KEY_TUNABLE }, | ||
| { "data-digest", LIBNVMF_KEY_BOOL, LIBNVMF_KEY_TUNABLE }, | ||
|
|
||
| /* security -- bound to (hostnqn, subsysnqn); never per-path */ |
There was a problem hiding this comment.
So that means our config.json is actually wrong, no? There it's model into the port section, which is 'per-path' or do I get this wrong again... confused...
| } | ||
|
|
||
| struct libnvmf_conf *libnvmf_conf_load(struct libnvme_global_ctx *ctx, | ||
| const char *path, int *err) |
There was a problem hiding this comment.
I would suggest to follow the error return code pattern used. That is the result is the last argument and not the return code.
| * | ||
| * Return: the resolved configuration, or NULL on error. | ||
| */ | ||
| struct libnvmf_config *libnvmf_config_read(struct libnvme_global_ctx *ctx, |
There was a problem hiding this comment.
I'd prefer to return an error code here and have the result as last argument.
|
Generally, I really like this. It's a big PR but that was to be expected. And it's good to see the whole picture. Also the diagnostic part is a nice feature to have. The public API is not that big after realizing what is exposed and what not. Good job! The main points from my first review pass is mostly nitpicking (function signatures and not using ccan list) My big question is about config.json: is this correctly modeled? From my current understanding it is not... and the other question is: do we want to keep |

Hi Daniel,
Here's the big PR I promised — ~4,400 lines total, but only ~1,400 of that is actual code and another ~1,400 tests; the rest is kdoc/comments, blank lines, and the design doc. It's self-contained: nothing calls into it yet, so no regression risk today. It's this size because it does two things the JSON blob didn't: real validation with
file:linediagnostics, and a drop-in file layout.Suggested order:
CONFIG.mdfirst (the design contract),config.h(the whole public surface),3.1 tokenizer: libnvme/config: add an internal INI reader
3.2 param bag: libnvme/config: add the parameter bag and key table
3.3 raw parser: libnvme/config: parse a config file into its raw model
3.4 cascade resolver: libnvme/config: resolve the cascade into the flat connection list
3.5 public API: libnvme/config: publish the resolved configuration
3.6 arg emitter: libnvme/config: add the connect argument emitter
host_iface/trsvcidcopy-paste mixup inlibnvmf_add_ctrl(), fixed while I was in the area.A second (much smaller) PR (write side +
nvme config convert) follows once this one lands.