Bug Summary

File:.build-ci/../discoverd/src/main.c
Warning:line 236, column 13
Although the value stored to 'read_devname' is used in the enclosing expression, the value is never actually read from 'read_devname'

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple x86_64-redhat-linux-gnu -O3 -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name main.c -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=deadcode -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -mrelocation-model static -mframe-pointer=none -fmath-errno -ffp-contract=on -fno-rounding-math -mconstructor-aliases -funwind-tables=2 -target-cpu x86-64 -tune-cpu generic -debugger-tuning=gdb -fdebug-compilation-dir=/__w/nvme-cli/nvme-cli/.build-ci -fcoverage-compilation-dir=/__w/nvme-cli/nvme-cli/.build-ci -resource-dir /usr/bin/../lib/clang/22 -include /__w/nvme-cli/nvme-cli/.build-ci/nvme-config.h -I discoverd/nvme-discoverd.p -I discoverd -I ../discoverd -I ../discoverd/src -I ccan -I ../ccan -I . -I .. -I libnvme/src -I ../libnvme/src -D _FILE_OFFSET_BITS=64 -D _GNU_SOURCE -D NVME_PATH="/usr/local/sbin/nvme" -U NDEBUG -internal-isystem /usr/bin/../lib/clang/22/include -internal-isystem /usr/local/include -internal-isystem /usr/bin/../lib/gcc/x86_64-redhat-linux/16/../../../../x86_64-redhat-linux/include -internal-externc-isystem /include -internal-externc-isystem /usr/include -std=gnu11 -ferror-limit 19 -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fcolor-diagnostics -vectorize-loops -vectorize-slp -analyzer-opt-analyze-headers -analyzer-output=html -faddrsig -fdwarf2-cfi-asm -o /__w/nvme-cli/nvme-cli/.build-ci/scan-results/2026-10-24-125112-639-1 -x c ../discoverd/src/main.c
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * This file is part of nvme-cli.
4 * Copyright (c) 2026 Dell Technologies Inc. or its subsidiaries.
5 *
6 * Authors: Martin Belanger <martin.belanger@dell.com>
7 */
8
9#include <dirent.h>
10#include <errno(*__errno_location ()).h>
11#include <getopt.h>
12#include <inttypes.h>
13#include <stdio.h>
14#include <stdlib.h>
15#include <string.h>
16#include <ifaddrs.h>
17
18#include <systemd/sd-bus.h>
19#include <systemd/sd-daemon.h>
20#include <systemd/sd-device.h>
21#include <systemd/sd-event.h>
22
23#include <ccan/list/list.h>
24#include <ccan/str/str.h>
25
26#include <daemon-util/log.h>
27#include <daemon-util/resolve.h>
28#include <daemon-util/work.h>
29#include <daemon-util/signals.h>
30#include <shared/array-util.h>
31#include <shared/cleanup-util.h>
32#include <shared/string-util.h>
33#include <shared/time-util.h>
34#include <nvme/config.h>
35#include <nvme/exclusion.h>
36#include <nvme/fabrics.h>
37#include <nvme/lib.h>
38#include <nvme/registry.h>
39#include <nvme/util.h>
40
41#include "inventory.h"
42#include "config.h"
43#include "ctx.h"
44#include "dlp.h"
45#include "events.h"
46#include "fc.h"
47#include "mdns.h"
48#include "state.h"
49#include "tid.h"
50#include "units.h"
51
52// Exponential backoff for failed (re)connect attempts: 1s, 2s, 4s, ... capped
53// at 5 min. A dynamically-discovered DC (not NBFT- or config-sourced — a
54// referral or FC-kickstart find) additionally gives up after
55// dc-giveup-timeout (default 72hours) of unbroken failure and is dropped
56// from tracking, since nothing but its own retries vouches for it any more.
57// Static and NBFT-sourced DCs represent deliberate admin/firmware intent
58// and always retry forever.
59#define RETRY_INITIAL_DELAY_SEC1 1
60#define RETRY_MAX_DELAY_SEC300 300
61
62// Name lookups of the fabrics configuration that run at the same time.
63#define RESOLVE_WORKERS8 8
64#define DLP_WORKERS4 4
65
66struct active_ctrl {
67 struct list_node entry;
68 char *unit_name; // "nvme-discoverd-<12hex>.service"
69 char *devname; // "nvmeX"; NULL until confirmed in sysfs
70 struct libnvmf_tid *tid;
71 bool_Bool is_dc;
72 unsigned int attempts; // consecutive failed (re)connect attempts
73 uint64_t giveup_at_usec; // 0 = no deadline armed (ctrl_arm_giveup)
74 sd_event_source *retry_timer; // NULL when no retry pending
75
76 /*
77 * EPCSD this DC's own referral entry carried in its parent's
78 * Discovery Log Page, if this DC was reached via referral. Fallback
79 * for when this DC's own self entry (SUBTYPE 03h) turns out to be
80 * absent from its own Discovery Log Page. Unset for primary DCs
81 * (statically configured, mDNS-discovered, or NBFT) — they have no
82 * parent.
83 */
84 bool_Bool parent_epcsd_known;
85 bool_Bool parent_epcsd; // meaningful only if parent_epcsd_known
86
87 // This DC's resolved "persistent" setting is "force": keep it
88 // connected regardless of what its own EPCSD bit reports.
89 bool_Bool force_persistent;
90 bool_Bool force_persistent_logged; // log_info_once() marker
91
92 sd_event_source *epcsd_poll_timer; // NULL when not EPCSD-parked
93
94 // Desired at the last release_undesired() pass, see there.
95 bool_Bool desired;
96
97 // A DC whose DLP was fetched since nvme-discoverd started.
98 bool_Bool dlp_fetched;
99
100 struct dmn_work *dlp_read; // NULL when no DLP read runs
101 bool_Bool dlp_again; // read the DLP again when this read ends
102};
103
104/*
105 * A controller from the file of last known desired controllers that is
106 * not decided yet. See decide_saved().
107 */
108struct saved_ctrl {
109 struct list_node entry;
110 char *source;
111 struct libnvmf_tid *tid;
112 struct libnvmf_tid *parent; // the DC that listed it, for "dlp"
113};
114
115static LIST_HEAD(g_saved)struct list_head g_saved = { { &(g_saved).n, &(g_saved
).n } }
;
116
117static LIST_HEAD(g_ctrls)struct list_head g_ctrls = { { &(g_ctrls).n, &(g_ctrls
).n } }
;
118static struct discoverd_ctx ctx;
119
120static struct active_ctrl *ctrl_find_by_unit(const char *unit_name)
121{
122 struct active_ctrl *e;
123
124 list_for_each(&g_ctrls, e, entry)for ((e) = list_node_to_off_(((void)"../discoverd/src/main.c"
":" "124", (&g_ctrls))->n.next, (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))); list_node_from_off_((void *)(e), (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))) != &((&g_ctrls))->n; (e) = list_node_to_off_
(list_node_from_off_((void *)(e), (((__builtin_offsetof(typeof
(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0)))))->next, (((__builtin_offsetof(typeof(*e), entry) +
((typeof(e->entry) *)0 != (struct list_node *)0))))))
{
125 if (streq(e->unit_name, unit_name)(strcmp((e->unit_name),(unit_name)) == 0))
126 return e;
127 }
128 return NULL((void*)0);
129}
130
131static struct active_ctrl *ctrl_find_by_devname(const char *devname)
132{
133 struct active_ctrl *e;
134
135 list_for_each(&g_ctrls, e, entry)for ((e) = list_node_to_off_(((void)"../discoverd/src/main.c"
":" "135", (&g_ctrls))->n.next, (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))); list_node_from_off_((void *)(e), (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))) != &((&g_ctrls))->n; (e) = list_node_to_off_
(list_node_from_off_((void *)(e), (((__builtin_offsetof(typeof
(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0)))))->next, (((__builtin_offsetof(typeof(*e), entry) +
((typeof(e->entry) *)0 != (struct list_node *)0))))))
{
136 if (shr_streq0(e->devname, devname))
137 return e;
138 }
139 return NULL((void*)0);
140}
141
142static void ctrl_free(struct active_ctrl *e)
143{
144 if (!e)
145 return;
146 sd_event_source_unref(e->retry_timer);
147 sd_event_source_unref(e->epcsd_poll_timer);
148 dmn_work_cancel(e->dlp_read);
149 tid_free(e->tid);
150 free(e->unit_name);
151 free(e->devname);
152 free(e);
153}
154
155static void schedule_release(bool_Bool check_exclusions);
156
157static int ctrl_add(const char *unit_name, const struct libnvmf_tid *t,
158 bool_Bool is_dc, const struct libnvmf_params *params)
159{
160 struct active_ctrl *e;
161
162 if (ctrl_find_by_unit(unit_name))
163 return 0; // already tracked
164
165 e = calloc(1, sizeof(*e));
166 if (!e)
167 return -ENOMEM12;
168
169 e->unit_name = strdup(unit_name);
170 e->tid = libnvmf_tid_dup(t);
171 e->is_dc = is_dc;
172 e->desired = inventory_is_desired(ctx.inventory, t);
173 e->force_persistent = params &&
174 shr_streqcase0(libnvmf_params_get(params, "persistent"),
175 "force");
176 if (!e->unit_name || !e->tid) {
177 ctrl_free(e);
178 return -ENOMEM12;
179 }
180
181 list_add(&g_ctrls, &e->entry)list_add_(&g_ctrls, &e->entry, "../discoverd/src/main.c"
":" "181")
;
182 schedule_release(false0); // the saved desired set changes
183 return 0;
184}
185
186static void ctrl_remove(struct active_ctrl *entry)
187{
188 list_del_init(&entry->entry)list_del_init_(&entry->entry, "../discoverd/src/main.c"
":" "188")
;
189 ctrl_free(entry);
190 schedule_release(false0); // the saved desired set changes
191}
192
193/*
194 * Release @unit_name's controller, and leave its connection up. The state
195 * goes first, so that the unit's ExecStop= has nothing to disconnect. Then
196 * the unit is stopped and the registry owner is cleared. @devname may be
197 * NULL when the controller is not connected.
198 */
199static void release_unit(const char *unit_name, const char *devname,
200 const struct libnvmf_tid *tid, const char *reason)
201{
202 state_remove_devid(unit_name);
203 if (devname) {
204 __cleanup_free__attribute__((cleanup(shr_freep))) char *owner_unit = state_read_unit(devname);
205
206 if (shr_streq0(owner_unit, unit_name))
207 state_remove_ctrl(devname);
208 }
209
210 if (unit_exists(ctx.umgr, unit_name))
211 unit_stop(ctx.umgr, unit_name);
212
213 /* Only clear an owner that is still ours. */
214 if (devname &&
215 !libnvmf_registry_attr_equal(ctx.nvme_ctx, devname, "owner",
216 "discoverd")) {
217 int r = libnvmf_registry_update(ctx.nvme_ctx, devname,
218 "owner", NULL((void*)0));
219
220 if (r < 0)
221 log_warn("%s | %s - failed to clear registry owner: %s",dmn_log_msg(DMN_LOG_WARN, "%s | %s - failed to clear registry owner: %s"
, libnvmf_tid_str(tid), devname, strerror(-r))
222 libnvmf_tid_str(tid), devname, strerror(-r))dmn_log_msg(DMN_LOG_WARN, "%s | %s - failed to clear registry owner: %s"
, libnvmf_tid_str(tid), devname, strerror(-r))
;
223 }
224
225 log_info("%s | %s - %s, released", libnvmf_tid_str(tid),dmn_log_msg(DMN_LOG_INFO, "%s | %s - %s, released", libnvmf_tid_str
(tid), devname ? devname : "-", reason)
226 devname ? devname : "-", reason)dmn_log_msg(DMN_LOG_INFO, "%s | %s - %s, released", libnvmf_tid_str
(tid), devname ? devname : "-", reason)
;
227}
228
229/* Release a tracked controller, and stop tracking it. */
230static void ctrl_release(struct active_ctrl *e, const char *reason)
231{
232 __cleanup_free__attribute__((cleanup(shr_freep))) char *read_devname = NULL((void*)0);
233 const char *devname = e->devname;
234
235 if (!devname)
236 devname = read_devname = unit_read_devid(e->unit_name);
Although the value stored to 'read_devname' is used in the enclosing expression, the value is never actually read from 'read_devname'
237
238 release_unit(e->unit_name, devname, e->tid, reason);
239 ctrl_remove(e);
240}
241
242static bool_Bool devname_matches_tid(const char *devname,
243 const struct libnvmf_tid *tid);
244
245static void saved_free(struct saved_ctrl *s)
246{
247 list_del_init(&s->entry)list_del_init_(&s->entry, "../discoverd/src/main.c" ":"
"247")
;
248 free(s->source);
249 tid_free(s->tid);
250 tid_free(s->parent);
251 free(s);
252}
253
254/*
255 * Decide a saved controller that nvme-discoverd did not adopt at startup.
256 * Returns true when it is decided, and false while it waits for the DLP of
257 * the DC that listed it.
258 */
259static bool_Bool decide_saved(struct saved_ctrl *s)
260{
261 __cleanup_free__attribute__((cleanup(shr_freep))) char *unit_name = tid_unit_name(s->tid);
262 __cleanup_free__attribute__((cleanup(shr_freep))) char *devname = NULL((void*)0);
263
264 if (!unit_name || ctrl_find_by_unit(unit_name) ||
265 inventory_is_desired(ctx.inventory, s->tid))
266 return true1;
267
268 if (streq(s->source, "dlp")(strcmp((s->source),("dlp")) == 0) && s->parent &&
269 inventory_is_desired(ctx.inventory, s->parent)) {
270 __cleanup_free__attribute__((cleanup(shr_freep))) char *dc_unit = tid_unit_name(s->parent);
271 struct active_ctrl *dc = dc_unit ?
272 ctrl_find_by_unit(dc_unit) : NULL((void*)0);
273
274 if (!dc || !dc->dlp_fetched)
275 return false0;
276 }
277
278 /* The kernel reuses device names: check it is still this one. */
279 devname = unit_read_devid(unit_name);
280 if (devname && !devname_matches_tid(devname, s->tid)) {
281 free(devname);
282 devname = NULL((void*)0);
283 }
284
285 /* No unit and no connection: nothing is left to release. */
286 if (!devname && !unit_exists(ctx.umgr, unit_name))
287 return true1;
288
289 release_unit(unit_name, devname, s->tid,
290 "no longer desired since the last run");
291 return true1;
292}
293
294/*
295 * Rewrite the file of last known desired controllers: the tracked
296 * controllers that are desired, and the saved ones not decided yet.
297 */
298static void save_desired(void)
299{
300 __cleanup_free__attribute__((cleanup(shr_freep))) char *content = NULL((void*)0);
301 struct active_ctrl *e;
302 struct saved_ctrl *s;
303 size_t size = 0;
304 FILE *f;
305 int r;
306
307 f = open_memstream(&content, &size);
308 if (!f)
309 return;
310
311 list_for_each(&g_ctrls, e, entry)for ((e) = list_node_to_off_(((void)"../discoverd/src/main.c"
":" "311", (&g_ctrls))->n.next, (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))); list_node_from_off_((void *)(e), (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))) != &((&g_ctrls))->n; (e) = list_node_to_off_
(list_node_from_off_((void *)(e), (((__builtin_offsetof(typeof
(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0)))))->next, (((__builtin_offsetof(typeof(*e), entry) +
((typeof(e->entry) *)0 != (struct list_node *)0))))))
{
312 const struct libnvmf_tid *parent;
313 const char *source;
314
315 if (!e->desired)
316 continue;
317 source = inventory_source(ctx.inventory, e->tid, &parent);
318 if (!source)
319 continue;
320 fprintf(f, "%s\t%s\t%s\n", source,
321 libnvmf_tid_get_canonical(e->tid),
322 parent ? libnvmf_tid_get_canonical(parent) : "-");
323 }
324 list_for_each(&g_saved, s, entry)for ((s) = list_node_to_off_(((void)"../discoverd/src/main.c"
":" "324", (&g_saved))->n.next, (((__builtin_offsetof
(typeof(*s), entry) + ((typeof(s->entry) *)0 != (struct list_node
*)0))))); list_node_from_off_((void *)(s), (((__builtin_offsetof
(typeof(*s), entry) + ((typeof(s->entry) *)0 != (struct list_node
*)0))))) != &((&g_saved))->n; (s) = list_node_to_off_
(list_node_from_off_((void *)(s), (((__builtin_offsetof(typeof
(*s), entry) + ((typeof(s->entry) *)0 != (struct list_node
*)0)))))->next, (((__builtin_offsetof(typeof(*s), entry) +
((typeof(s->entry) *)0 != (struct list_node *)0))))))
325 fprintf(f, "%s\t%s\t%s\n", s->source,
326 libnvmf_tid_get_canonical(s->tid),
327 s->parent ? libnvmf_tid_get_canonical(s->parent) : "-");
328
329 if (fclose(f) != 0)
330 return;
331
332 r = state_write_desired(content);
333 if (r < 0)
334 log_warn("cannot save the desired controllers: %s",dmn_log_msg(DMN_LOG_WARN, "cannot save the desired controllers: %s"
, strerror(-r))
335 strerror(-r))dmn_log_msg(DMN_LOG_WARN, "cannot save the desired controllers: %s"
, strerror(-r))
;
336}
337
338/*
339 * Read the file of last known desired controllers. Discovered DCs become
340 * desired again, as they were before the restart. The others wait in
341 * g_saved for decide_saved().
342 */
343static void load_saved(void)
344{
345 __cleanup_free__attribute__((cleanup(shr_freep))) char *content = state_read_desired();
346 char *line, *next;
347
348 for (line = content; line && *line; line = next) {
349 char *source, *canon, *parent;
350 struct saved_ctrl *s;
351
352 next = strchr(line, '\n')_Generic (0 ? (line) : (void *) 1, const void *: (const char *
) (strchr (line, '\n')), default: strchr (line, '\n'))
;
353 if (next)
354 *next++ = '\0';
355 else
356 next = line + strlen(line);
357
358 source = strtok(line, "\t");
359 canon = strtok(NULL((void*)0), "\t");
360 parent = strtok(NULL((void*)0), "\t");
361 if (!source || !canon || !parent)
362 continue;
363
364 s = calloc(1, sizeof(*s));
365 if (!s)
366 return;
367 list_node_init(&s->entry);
368 s->source = strdup(source);
369 if (!s->source ||
370 libnvmf_tid_parse(ctx.nvme_ctx, canon, &s->tid) < 0 ||
371 (!streq(parent, "-")(strcmp((parent),("-")) == 0) &&
372 libnvmf_tid_parse(ctx.nvme_ctx, parent, &s->parent) < 0)) {
373 saved_free(s);
374 continue;
375 }
376
377 if (streq(s->source, "discovered")(strcmp((s->source),("discovered")) == 0)) {
378 inventory_add_discovered_dc(ctx.inventory, s->tid);
379 saved_free(s);
380 continue;
381 }
382 list_add_tail(&g_saved, &s->entry)list_add_tail_(&g_saved, &s->entry, "../discoverd/src/main.c"
":" "382")
;
383 }
384}
385
386static bool_Bool release_pending;
387static bool_Bool release_check_exclusions;
388
389/*
390 * Release the controllers that are no longer desired: desired at the last
391 * pass, and not desired now. A controller that was never desired, such as
392 * an mDNS DC whose DLP is not fetched yet, is never released here. With
393 * @release_check_exclusions, also release the controllers that the
394 * exclusion list now matches. NBFT controllers are never released.
395 * While a host name of the fabrics configuration is not resolved yet, its
396 * controller looks not desired, so the pass waits for on_config_lookup().
397 */
398static int release_undesired(sd_event_source *src,
399 void *user_data __attribute__((unused)))
400{
401 struct active_ctrl *e, *next;
402 struct saved_ctrl *s, *snext;
403 bool_Bool check_exclusions;
404
405 release_pending = false0;
406 sd_event_source_disable_unref(src);
407
408 if (inventory_config_pending(ctx.inventory))
409 return 0;
410
411 check_exclusions = release_check_exclusions;
412 release_check_exclusions = false0;
413
414 list_for_each_safe(&g_ctrls, e, next, entry)for ((e) = list_node_to_off_(((void)"../discoverd/src/main.c"
":" "414", (&g_ctrls))->n.next, (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))), (next) = list_node_to_off_(list_node_from_off_((e)
, (((__builtin_offsetof(typeof(*e), entry) + ((typeof(e->entry
) *)0 != (struct list_node *)0)))))->next, (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))); list_node_from_off_((e), (((__builtin_offsetof(typeof
(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))) != &((&g_ctrls))->n; (e) = (next), (next
) = list_node_to_off_(list_node_from_off_((e), (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0)))))->next, (((__builtin_offsetof(typeof(*e), entry) +
((typeof(e->entry) *)0 != (struct list_node *)0))))))
{
415 bool_Bool desired = inventory_is_desired(ctx.inventory, e->tid);
416
417 if (inventory_is_nbft(ctx.inventory, e->tid))
418 continue;
419
420 if (check_exclusions &&
421 libnvmf_exclusion_match(ctx.nvme_ctx, e->tid)) {
422 ctrl_release(e, "excluded");
423 continue;
424 }
425
426 if (e->desired && !desired) {
427 ctrl_release(e, "no longer desired");
428 continue;
429 }
430
431 e->desired = desired;
432 }
433
434 list_for_each_safe(&g_saved, s, snext, entry)for ((s) = list_node_to_off_(((void)"../discoverd/src/main.c"
":" "434", (&g_saved))->n.next, (((__builtin_offsetof
(typeof(*s), entry) + ((typeof(s->entry) *)0 != (struct list_node
*)0))))), (snext) = list_node_to_off_(list_node_from_off_((s
), (((__builtin_offsetof(typeof(*s), entry) + ((typeof(s->
entry) *)0 != (struct list_node *)0)))))->next, (((__builtin_offsetof
(typeof(*s), entry) + ((typeof(s->entry) *)0 != (struct list_node
*)0))))); list_node_from_off_((s), (((__builtin_offsetof(typeof
(*s), entry) + ((typeof(s->entry) *)0 != (struct list_node
*)0))))) != &((&g_saved))->n; (s) = (snext), (snext
) = list_node_to_off_(list_node_from_off_((s), (((__builtin_offsetof
(typeof(*s), entry) + ((typeof(s->entry) *)0 != (struct list_node
*)0)))))->next, (((__builtin_offsetof(typeof(*s), entry) +
((typeof(s->entry) *)0 != (struct list_node *)0))))))
{
435 if (decide_saved(s))
436 saved_free(s);
437 }
438
439 save_desired();
440
441 return 0;
442}
443
444/*
445 * Run release_undesired() from the event loop, after the change that
446 * called this is complete. Several changes before the pass share it.
447 */
448static void schedule_release(bool_Bool check_exclusions)
449{
450 sd_event_source *src;
451 int r;
452
453 release_check_exclusions |= check_exclusions;
454 if (release_pending)
455 return;
456
457 r = sd_event_add_defer(ctx.event, &src, release_undesired, NULL((void*)0));
458 if (r < 0) {
459 log_warn("cannot schedule the release check: %s",dmn_log_msg(DMN_LOG_WARN, "cannot schedule the release check: %s"
, strerror(-r))
460 strerror(-r))dmn_log_msg(DMN_LOG_WARN, "cannot schedule the release check: %s"
, strerror(-r))
;
461 return;
462 }
463 release_pending = true1;
464}
465
466/*
467 * Take the current desired state as the new reference, without releasing
468 * anything. For inventory changes that must not release, such as giving up
469 * on a DC.
470 */
471static void refresh_desired(void)
472{
473 struct active_ctrl *e;
474
475 list_for_each(&g_ctrls, e, entry)for ((e) = list_node_to_off_(((void)"../discoverd/src/main.c"
":" "475", (&g_ctrls))->n.next, (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))); list_node_from_off_((void *)(e), (((__builtin_offsetof
(typeof(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0))))) != &((&g_ctrls))->n; (e) = list_node_to_off_
(list_node_from_off_((void *)(e), (((__builtin_offsetof(typeof
(*e), entry) + ((typeof(e->entry) *)0 != (struct list_node
*)0)))))->next, (((__builtin_offsetof(typeof(*e), entry) +
((typeof(e->entry) *)0 != (struct list_node *)0))))))
476 e->desired = inventory_is_desired(ctx.inventory, e->tid);
477}
478
479/* One connected controller, as sysfs reports it. */
480struct scanned_ctrl {
481 char *devname;
482 struct libnvmf_tid *tid;
483 bool_Bool is_dc;
484 bool_Bool live;
485};
486
487SHR_PTRARRAY_DEFINE(scanned_ctrl_list, struct scanned_ctrl)struct scanned_ctrl_list { struct scanned_ctrl **items; size_t
len; size_t cap; }; static inline int scanned_ctrl_list_append
(struct scanned_ctrl_list *a, struct scanned_ctrl *item) { do
{ (void) sizeof(char [1 - 2*!(sizeof(struct scanned_ctrl_list
) == sizeof(struct shr_ptrarray))]); } while(0); return shr_ptrarray_append
( (struct shr_ptrarray *)a, item); } static inline void scanned_ctrl_list_free
(struct scanned_ctrl_list *a) { shr_ptrarray_free((struct shr_ptrarray
*)a); }
;
488
489static void scanned_ctrl_free(struct scanned_ctrl *sc)
490{
491 if (!sc)
492 return;
493 tid_free(sc->tid);
494 free(sc->devname);
495 free(sc);
496}
497
498/*
499 * Snapshot of every connected controller plus the host's interface
500 * addresses. Matching one candidate needs both, and both are expensive to
501 * build: a sysfs walk reads six attributes per controller, and getifaddrs()
502 * dumps the whole address table. A caller about to test many candidates --
503 * every entry of a Discovery Log Page, every desired connection at startup
504 * -- builds one snapshot and passes it down, instead of paying for both per
505 * candidate.
506 *
507 * The snapshot is deliberately not cached across calls. Addresses and
508 * controllers change under a daemon that runs for weeks, and a stale
509 * snapshot produces wrong match results rather than merely slow ones.
510 */
511struct conn_scan {
512 struct ifaddrs *iface_list;
513 struct scanned_ctrl_list ctrls;
514};
515
516static void conn_scan_load(struct conn_scan *scan);
517static void conn_scan_free(struct conn_scan *scan);
518static const char *find_devname_for_tid(const struct conn_scan *scan,
519 const struct libnvmf_tid *tid);
520
521/* Frees a conn_scan on scope exit; a zeroed scan is a safe no-op. */
522#define __cleanup_conn_scan__attribute__((cleanup(conn_scan_free))) __attribute__((cleanup(conn_scan_free)))
523
524/*
525 * Exclusion + registry-owner check — called before every connect decision.
526 *
527 * Exclusion applies to NBFT-sourced controllers too: the exclusion list is
528 * the host administrator's explicit, root-only instruction, and is the
529 * supported way to take a boot path out of service for testing or
530 * maintenance. owner=nbft still protects a boot-path controller from every
531 * *other* orchestrator via the registry — it just does not override the
532 * local admin's own exclusion entry.
533 *
534 * The registry check skips a controller another orchestrator (e.g.
535 * nvme-stas) already owns. "discoverd" and "nbft" are discoverd's own
536 * registry owner strings (see unit_start_dc()/unit_start_ioc() in units.c),
537 * so a controller discoverd itself owns is never skipped here.
538 *
539 * @scan is a snapshot the caller built before a loop, or NULL to build a
540 * throwaway one for this call. It is only consulted when @known_devname
541 * is NULL.
542 *
543 * @known_devname is the existing (or just-removed) device name for @tid,
544 * when the caller already has it in hand (on_nvme_remove()); NULL
545 * otherwise, in which case this function resolves it itself from @scan
546 * (find_devname_for_tid()). Either way the registry is checked directly by
547 * device name, never through libnvme's in-process topology tree: that tree
548 * is only populated by a caller that has just run libnvme_scan_topology()
549 * (true for the CLI's one-shot fabrics commands, never true for this
550 * long-running daemon), so a tree-based match would silently report "no
551 * owner" for every already-connected controller discoverd didn't itself
552 * just scan.
553 */
554static bool_Bool should_connect(const struct conn_scan *scan,
555 const struct libnvmf_tid *tid,
556 const char *known_devname)
557{
558 __cleanup_conn_scan__attribute__((cleanup(conn_scan_free))) struct conn_scan local = { 0 };
559 __cleanup_free__attribute__((cleanup(shr_freep))) char *owner = NULL((void*)0);
560 const char *devname = known_devname;
561 int r;
562
563 if (libnvmf_exclusion_match(ctx.nvme_ctx, tid)) {
564 log_info("%s - excluded, skipping", libnvmf_tid_str(tid))dmn_log_msg(DMN_LOG_INFO, "%s - excluded, skipping", libnvmf_tid_str
(tid))
;
565 return false0;
566 }
567
568 if (!devname) {
569 if (!scan) {
570 conn_scan_load(&local);
571 scan = &local;
572 }
573 devname = find_devname_for_tid(scan, tid);
574 }
575
576 if (devname) {
577 r = libnvmf_registry_retrieve(ctx.nvme_ctx, devname, "owner",
578 &owner);
579 if (r == -ENOENT2)
580 r = 0;
581 } else {
582 r = 0; // nothing currently connected matching tid
583 }
584
585 if (r < 0) {
586 log_warn("%s - failed to check registry owner: %s",dmn_log_msg(DMN_LOG_WARN, "%s - failed to check registry owner: %s"
, libnvmf_tid_str(tid), strerror(-r))
587 libnvmf_tid_str(tid), strerror(-r))dmn_log_msg(DMN_LOG_WARN, "%s - failed to check registry owner: %s"
, libnvmf_tid_str(tid), strerror(-r))
;
588 return false0;
589 }
590 if (owner && !streq(owner, "discoverd")(strcmp((owner),("discoverd")) == 0) && !streq(owner, "nbft")(strcmp((owner),("nbft")) == 0)) {
591 log_info("%s - owned by '%s', skipping",dmn_log_msg(DMN_LOG_INFO, "%s - owned by '%s', skipping", libnvmf_tid_str
(tid), owner)
592 libnvmf_tid_str(tid), owner)dmn_log_msg(DMN_LOG_INFO, "%s - owned by '%s', skipping", libnvmf_tid_str
(tid), owner)
;
593 return false0;
594 }
595
596 return true1;
597}
598
599/*
600 * Resolved connect parameters for @t: a statically configured connection's
601 * own params, or — for anything discoverd found on its own (NBFT, DLP, FC
602 * kickstart) — the discovered-controller defaults for the scope @via_dc
603 * was learned through (NULL if @t has no configured parent DC either),
604 * with the KX-HMAC-CHAP secrets of the section that has @t's NQN.
605 * See libnvmf_config_get_discovered_params() in <nvme/config.h>.
606 */
607static const struct libnvmf_params *params_for(
608 const struct libnvmf_tid *t, bool_Bool is_dc,
609 const struct libnvmf_config_conn *via_dc)
610{
611 const struct libnvmf_config_conn *conn =
612 inventory_config_conn_for(ctx.inventory, t);
613 const struct libnvmf_params *params;
614
615 if (conn)
616 return libnvmf_config_conn_get_params(conn);
617 if (!ctx.fabrics_cfg)
618 return NULL((void*)0);
619 if (libnvmf_config_get_discovered_params(ctx.fabrics_cfg,
620 libnvmf_config_conn_get_hostnqn(via_dc), is_dc,
621 libnvmf_tid_get_subsysnqn(t), &params))
622 return NULL((void*)0);
623
624 return params;
625}
626
627static void dlp_fetch(struct active_ctrl *e);
628static bool_Bool devname_matches_tid(const char *devname,
629 const struct libnvmf_tid *tid);
630
631/* Fetch the DLP of an adopted DC. Runs once, from the event loop. */
632static int adopted_dc_fetch(sd_event_source *src, void *user_data)
633{
634 char *unit_name = user_data;
635 struct active_ctrl *e = ctrl_find_by_unit(unit_name);
636
637 if (e)
638 dlp_fetch(e);
639
640 free(unit_name);
641 sd_event_source_disable_unref(src);
642
643 return 0;
644}
645
646/*
647 * Track a unit this daemon started before it restarted. The connection is
648 * already up and @devname names it. Takes ownership of @devname.
649 */
650static void adopt_ctrl(const char *unit_name, const struct libnvmf_tid *tid,
651 bool_Bool is_dc, const struct libnvmf_params *params,
652 char *devname)
653{
654 struct active_ctrl *e;
655
656 ctrl_add(unit_name, tid, is_dc, params);
657 e = ctrl_find_by_unit(unit_name);
658 if (!e) {
659 free(devname);
660 return;
661 }
662 e->devname = devname;
663
664 log_info("%s | %s - adopted, already connected",dmn_log_msg(DMN_LOG_INFO, "%s | %s - adopted, already connected"
, libnvmf_tid_str(tid), devname)
665 libnvmf_tid_str(tid), devname)dmn_log_msg(DMN_LOG_INFO, "%s | %s - adopted, already connected"
, libnvmf_tid_str(tid), devname)
;
666
667 /*
668 * An adopted DC produces no device-add event, so fetch its DLP here.
669 * Otherwise its DLP-sourced IOCs never enter the desired set and are
670 * not reconnected if they drop. Defer the fetch: a referral DC is
671 * adopted while its parent's DLP is being processed, and the parent's
672 * entries reach the inventory only when that is done.
673 */
674 if (is_dc) {
675 sd_event_source *src;
676 char *name = strdup(unit_name);
677 int r = -ENOMEM12;
678
679 if (name)
680 r = sd_event_add_defer(ctx.event, &src,
681 adopted_dc_fetch, name);
682 if (r < 0) {
683 log_warn("%s - cannot defer DLP fetch: %s",dmn_log_msg(DMN_LOG_WARN, "%s - cannot defer DLP fetch: %s", libnvmf_tid_str
(tid), strerror(-r))
684 libnvmf_tid_str(tid), strerror(-r))dmn_log_msg(DMN_LOG_WARN, "%s - cannot defer DLP fetch: %s", libnvmf_tid_str
(tid), strerror(-r))
;
685 free(name);
686 dlp_fetch(e);
687 }
688 }
689}
690
691/*
692 * Start a transient unit for @tid (as a DC or an IOC) and track it, or adopt
693 * the unit if a previous run of this daemon left it connected. Skipped when
694 * a unit for this TID is already tracked, so that the same IOC behind two
695 * DCs does not issue a duplicate StartTransient. The caller is responsible
696 * for the should_connect() decision. @via_dc is the parent DC's config
697 * connection, if @tid was learned via that DC's Discovery Log Page; NULL
698 * otherwise.
699 */
700static void start_ctrl(const struct libnvmf_tid *tid, bool_Bool is_dc,
701 const struct libnvmf_config_conn *via_dc)
702{
703 __cleanup_free__attribute__((cleanup(shr_freep))) char *unit_name = tid_unit_name(tid);
704 const struct libnvmf_params *params;
705 bool_Bool is_nbft;
706 int r;
707
708 if (!unit_name)
709 return;
710 if (ctrl_find_by_unit(unit_name))
711 return; // already tracked — no duplicate connect
712
713 params = params_for(tid, is_dc, via_dc);
714
715 /*
716 * A transient unit outlives the daemon. Starting it again fails with
717 * -EEXIST, and the recovery for that stops the unit, which
718 * disconnects. So adopt it if its recorded device is still this
719 * connection. Otherwise the unit is stale, and the -EEXIST recovery
720 * below replaces it.
721 *
722 * The kernel reuses nvmeN names, so the recorded device may now be
723 * another orchestrator's connection. Delete the stale state first:
724 * ExecStop= disconnects whatever device it records.
725 */
726 if (unit_exists(ctx.umgr, unit_name)) {
727 char *devname = unit_read_devid(unit_name);
728
729 if (devname && devname_matches_tid(devname, tid)) {
730 adopt_ctrl(unit_name, tid, is_dc, params, devname);
731 return;
732 }
733 state_remove_devid(unit_name);
734 if (devname) {
735 __cleanup_free__attribute__((cleanup(shr_freep))) char *owner_unit =
736 state_read_unit(devname);
737
738 if (shr_streq0(owner_unit, unit_name))
739 state_remove_ctrl(devname);
740 }
741 free(devname);
742 }
743
744 is_nbft = inventory_is_nbft(ctx.inventory, tid);
745
746 r = is_dc ? unit_start_dc(ctx.umgr, tid, params, is_nbft)
747 : unit_start_ioc(ctx.umgr, tid, params, is_nbft);
748 if (r >= 0) {
749 ctrl_add(unit_name, tid, is_dc, params);
750 log_dbg("%s: requested %s unit", libnvmf_tid_str(tid),dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s: requested %s unit",
__func__, libnvmf_tid_str(tid), is_dc ? "DC" : "IOC")
751 is_dc ? "DC" : "IOC")dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s: requested %s unit",
__func__, libnvmf_tid_str(tid), is_dc ? "DC" : "IOC")
;
752 } else {
753 log_warn("%s - failed to start %s unit: %s",dmn_log_msg(DMN_LOG_WARN, "%s - failed to start %s unit: %s",
libnvmf_tid_str(tid), is_dc ? "DC" : "IOC", strerror(-r))
754 libnvmf_tid_str(tid), is_dc ? "DC" : "IOC",dmn_log_msg(DMN_LOG_WARN, "%s - failed to start %s unit: %s",
libnvmf_tid_str(tid), is_dc ? "DC" : "IOC", strerror(-r))
755 strerror(-r))dmn_log_msg(DMN_LOG_WARN, "%s - failed to start %s unit: %s",
libnvmf_tid_str(tid), is_dc ? "DC" : "IOC", strerror(-r))
;
756 }
757}
758
759struct dlp_fetch_ctx {
760 const struct libnvmf_config_conn *via_dc; // dc_tid's own conn, if any
761 const struct conn_scan *scan; // shared by every entry's connect check
762 struct tid_list iocs; // for the inventory
763 struct tid_list referrals; // for the inventory
764 int hops; // the DC's referral hops from its source, -1 if none
765 bool_Bool self_seen;
766 bool_Bool epcsd; // meaningful only if self_seen
767};
768
769static void dlp_ioc_callback(const struct libnvmf_tid *t, void *user_data)
770{
771 struct dlp_fetch_ctx *fctx = user_data;
772 struct libnvmf_tid *dup;
773
774 dup = libnvmf_tid_dup(t);
775 if (!dup || tid_list_append(&fctx->iocs, dup) < 0) {
776 tid_free(dup);
777 return;
778 }
779
780 if (should_connect(fctx->scan, t, NULL((void*)0)))
781 start_ctrl(t, false0, fctx->via_dc);
782}
783
784/*
785 * Record the EPCSD bit @t's own referral entry carried in its parent's
786 * Discovery Log Page, as a fallback for when @t is later connected to and
787 * its own self entry turns out to be absent. A no-op if @t is not tracked
788 * yet (should_connect() rejected it, or start_ctrl() failed).
789 */
790static void record_parent_epcsd(const struct libnvmf_tid *t, bool_Bool epcsd)
791{
792 char *unit_name = tid_unit_name(t);
793 struct active_ctrl *e;
794
795 if (!unit_name)
796 return;
797
798 e = ctrl_find_by_unit(unit_name);
799 if (e) {
800 e->parent_epcsd_known = true1;
801 e->parent_epcsd = epcsd;
802 }
803 free(unit_name);
804}
805
806static void dlp_dc_callback(const struct libnvmf_tid *t, bool_Bool epcsd,
807 void *user_data)
808{
809 struct dlp_fetch_ctx *fctx = user_data;
810 struct libnvmf_tid *dup;
811
812 dup = libnvmf_tid_dup(t);
813 if (!dup || tid_list_append(&fctx->referrals, dup) < 0)
814 tid_free(dup);
815
816 /*
817 * Follow a referral only within the hop limit, as the libnvme
818 * discovery walk does. The referrals of a DC that is no longer
819 * desired are not followed either.
820 */
821 if (fctx->hops < 0)
822 return;
823 if (fctx->hops >= INVENTORY_MAX_REFERRAL_HOPS8) {
824 log_info("%s - referral %d hops from its source, not followed",dmn_log_msg(DMN_LOG_INFO, "%s - referral %d hops from its source, not followed"
, libnvmf_tid_str(t), fctx->hops + 1)
825 libnvmf_tid_str(t), fctx->hops + 1)dmn_log_msg(DMN_LOG_INFO, "%s - referral %d hops from its source, not followed"
, libnvmf_tid_str(t), fctx->hops + 1)
;
826 return;
827 }
828
829 if (should_connect(fctx->scan, t, NULL((void*)0))) {
830 start_ctrl(t, true1, fctx->via_dc);
831 record_parent_epcsd(t, epcsd);
832 }
833}
834
835static void dlp_self_callback(bool_Bool epcsd, void *user_data)
836{
837 struct dlp_fetch_ctx *fctx = user_data;
838
839 fctx->self_seen = true1;
840 fctx->epcsd = epcsd;
841}
842
843/*
844 * Self entry seen: its own EPCSD. Else the parent's referral-entry view
845 * of this DC, if any. Else assume EPCSD=0, matching core libnvme's
846 * dc_decide().
847 */
848static bool_Bool dc_effective_epcsd(const struct dlp_fetch_ctx *fctx,
849 const struct active_ctrl *e)
850{
851 if (fctx->self_seen)
852 return fctx->epcsd;
853 if (e && e->parent_epcsd_known)
854 return e->parent_epcsd;
855 return false0;
856}
857
858static void epcsd_park(struct active_ctrl *e);
859
860// Process the DLP of the DC @e.
861static void process_dlp(struct active_ctrl *e,
862 const struct nvmf_discovery_log *log)
863{
864 __cleanup_conn_scan__attribute__((cleanup(conn_scan_free))) struct conn_scan scan = { 0 };
865 const struct libnvmf_tid *dc_tid = e->tid;
866 struct dlp_fetch_ctx fctx = {
867 .via_dc = inventory_config_conn_for(ctx.inventory, dc_tid),
868 .hops = inventory_referral_hops(ctx.inventory, dc_tid),
869 };
870 bool_Bool epcsd;
871
872 // One snapshot for every entry this log page turns out to hold.
873 conn_scan_load(&scan);
874 fctx.scan = &scan;
875
876 dlp_process_log(log, dc_tid, dlp_ioc_callback, dlp_dc_callback,
877 dlp_self_callback, &fctx);
878
879 epcsd = dc_effective_epcsd(&fctx, e);
880 log_dbg("%s: self entry %s, effective EPCSD=%d",dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s: self entry %s, effective EPCSD=%d"
, __func__, libnvmf_tid_str(dc_tid), fctx.self_seen ? "seen" :
"absent", epcsd)
881 libnvmf_tid_str(dc_tid), fctx.self_seen ? "seen" : "absent",dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s: self entry %s, effective EPCSD=%d"
, __func__, libnvmf_tid_str(dc_tid), fctx.self_seen ? "seen" :
"absent", epcsd)
882 epcsd)dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s: self entry %s, effective EPCSD=%d"
, __func__, libnvmf_tid_str(dc_tid), fctx.self_seen ? "seen" :
"absent", epcsd)
;
883
884 if (e->is_dc && !epcsd) {
885 if (e->force_persistent)
886 log_info_once(&e->force_persistent_logged,dmn_log_msg_once(&e->force_persistent_logged, DMN_LOG_INFO
, "%s - EPCSD=0, but persistent=force: staying connected", libnvmf_tid_str
(dc_tid))
887 "%s - EPCSD=0, but persistent=force: staying connected",dmn_log_msg_once(&e->force_persistent_logged, DMN_LOG_INFO
, "%s - EPCSD=0, but persistent=force: staying connected", libnvmf_tid_str
(dc_tid))
888 libnvmf_tid_str(dc_tid))dmn_log_msg_once(&e->force_persistent_logged, DMN_LOG_INFO
, "%s - EPCSD=0, but persistent=force: staying connected", libnvmf_tid_str
(dc_tid))
;
889 else
890 epcsd_park(e);
891 }
892
893 // A log page without IOC entries replaces the last list.
894 if (tid_list_append(&fctx.iocs, NULL((void*)0)) == 0 &&
895 tid_list_append(&fctx.referrals, NULL((void*)0)) == 0) {
896 inventory_update_dlp(ctx.inventory, dc_tid, fctx.iocs.items,
897 fctx.referrals.items);
898 e->dlp_fetched = true1;
899 schedule_release(false0);
900 } else {
901 tid_list_free_items(&fctx.iocs);
902 tid_list_free_items(&fctx.referrals);
903 }
904}
905
906static int log_level(void)
907{
908 return ctx.force_debug ? DMN_LOG_DEBUG : ctx.cfg->debug_level;
909}
910
911static void on_dlp_read(int ret, const struct nvmf_discovery_log *log,
912 void *user_data)
913{
914 struct active_ctrl *e = user_data;
915
916 e->dlp_read = NULL((void*)0);
917
918 /*
919 * A failed read tells nothing about EPCSD or the DC's entries, so
920 * keep the DC and its last list.
921 */
922 if (ret < 0)
923 log_warn("%s | %s - cannot read the discovery log: %s",dmn_log_msg(DMN_LOG_WARN, "%s | %s - cannot read the discovery log: %s"
, libnvmf_tid_str(e->tid), e->devname ? e->devname :
"-", strerror(-ret))
924 libnvmf_tid_str(e->tid), e->devname ? e->devname : "-",dmn_log_msg(DMN_LOG_WARN, "%s | %s - cannot read the discovery log: %s"
, libnvmf_tid_str(e->tid), e->devname ? e->devname :
"-", strerror(-ret))
925 strerror(-ret))dmn_log_msg(DMN_LOG_WARN, "%s | %s - cannot read the discovery log: %s"
, libnvmf_tid_str(e->tid), e->devname ? e->devname :
"-", strerror(-ret))
;
926 else
927 process_dlp(e, log);
928
929 if (e->dlp_again) {
930 e->dlp_again = false0;
931 dlp_fetch(e);
932 }
933}
934
935/*
936 * Read the DLP of the DC @e, then process it. The read waits for the DC,
937 * so it runs in a worker and the event loop goes on. One read at a time
938 * per DC: a request during a read reads the DLP again when it ends.
939 */
940static void dlp_fetch(struct active_ctrl *e)
941{
942 int r;
943
944 if (!e->devname)
945 return;
946
947 if (e->dlp_read) {
948 e->dlp_again = true1;
949 return;
950 }
951
952 r = dlp_read(ctx.dlp_pool, e->devname, log_level(), on_dlp_read, e,
953 &e->dlp_read);
954 if (r < 0)
955 log_warn("%s | %s - cannot read the discovery log: %s",dmn_log_msg(DMN_LOG_WARN, "%s | %s - cannot read the discovery log: %s"
, libnvmf_tid_str(e->tid), e->devname, strerror(-r))
956 libnvmf_tid_str(e->tid), e->devname, strerror(-r))dmn_log_msg(DMN_LOG_WARN, "%s | %s - cannot read the discovery log: %s"
, libnvmf_tid_str(e->tid), e->devname, strerror(-r))
;
957}
958
959/*
960 * Arm a dynamically-discovered DC's give-up deadline the first time it is
961 * seen failing to (re)connect. Static/NBFT-sourced DCs and IOCs never get
962 * one and retry forever, and so does a dynamic DC when dc-giveup-timeout
963 * is configured to infinity.
964 *
965 * Note the two distinct zeros: giveup_at_usec == 0 means no deadline is
966 * armed, while a dc-giveup-timeout of 0 means give up on the first
967 * failure. The latter still arms, because now + 0 is not 0.
968 */
969static void ctrl_arm_giveup(struct active_ctrl *e)
970{
971 uint64_t now;
972
973 if (e->giveup_at_usec || !e->is_dc)
974 return;
975 if (inventory_is_nbft(ctx.inventory, e->tid) ||
976 inventory_config_conn_for(ctx.inventory, e->tid))
977 return;
978 if (ctx.cfg->dc_giveup_timeout_usec == SHR_USEC_INFINITY(18446744073709551615UL))
979 return;
980
981 if (sd_event_now(ctx.event, CLOCK_BOOTTIME7, &now) < 0)
982 return;
983
984 // A span so large the deadline would wrap means forever, not now.
985 if (ctx.cfg->dc_giveup_timeout_usec >= SHR_USEC_INFINITY(18446744073709551615UL) - now)
986 return;
987
988 e->giveup_at_usec = now + ctx.cfg->dc_giveup_timeout_usec;
989}
990
991static uint64_t backoff_delay_usec(unsigned int attempts)
992{
993 uint64_t sec = RETRY_INITIAL_DELAY_SEC1;
994 unsigned int i;
995
996 for (i = 0; i < attempts && sec < RETRY_MAX_DELAY_SEC300; i++)
997 sec *= 2;
998 if (sec > RETRY_MAX_DELAY_SEC300)
999 sec = RETRY_MAX_DELAY_SEC300;
1000 return sec * UINT64_C(1000000)1000000UL;
1001}
1002
1003static int retry_timeout(sd_event_source *src, uint64_t usec, void *user_data);
1004
1005static int schedule_retry(struct active_ctrl *e)
1006{
1007 uint64_t now, delay;
1008 int r;
1009
1010 if (e->retry_timer)
1011 return 0; // already scheduled
1012
1013 ctrl_arm_giveup(e);
1014
1015 r = sd_event_now(ctx.event, CLOCK_BOOTTIME7, &now);
1016 if (r < 0)
1017 return r;
1018
1019 delay = backoff_delay_usec(e->attempts);
1020 e->attempts++;
1021
1022 return sd_event_add_time(ctx.event, &e->retry_timer, CLOCK_BOOTTIME7,
1023 now + delay, 0, retry_timeout, e);
1024}
1025
1026/*
1027 * Restart @e's unit (parameters are baked in). Falls back to a fresh
1028 * StartTransient — using the top-level discovered-controller scope, not
1029 * @e's original parent DC's scope, a deliberate simplification since
1030 * active_ctrl does not track that borrowed config_conn across a possible
1031 * SIGHUP config reload — if the unit was garbage-collected.
1032 */
1033static int restart_or_start(struct active_ctrl *e)
1034{
1035 int r = unit_restart(ctx.umgr, e->unit_name);
1036
1037 if (r < 0) {
1038 const struct libnvmf_params *params =
1039 params_for(e->tid, e->is_dc, NULL((void*)0));
1040 bool_Bool is_nbft = inventory_is_nbft(ctx.inventory, e->tid);
1041
1042 if (e->is_dc)
1043 r = unit_start_dc(ctx.umgr, e->tid, params, is_nbft);
1044 else
1045 r = unit_start_ioc(ctx.umgr, e->tid, params, is_nbft);
1046 }
1047 return r;
1048}
1049
1050static int retry_timeout(sd_event_source *src,
1051 uint64_t usec __attribute__((unused)),
1052 void *user_data)
1053{
1054 struct active_ctrl *e = user_data;
1055 uint64_t now;
1056 int r;
1057
1058 sd_event_source_unref(src);
1059 e->retry_timer = NULL((void*)0);
1060
1061 if (!inventory_is_desired(ctx.inventory, e->tid)) {
1062 log_info("%s - no longer desired, not retrying",dmn_log_msg(DMN_LOG_INFO, "%s - no longer desired, not retrying"
, libnvmf_tid_str(e->tid))
1063 libnvmf_tid_str(e->tid))dmn_log_msg(DMN_LOG_INFO, "%s - no longer desired, not retrying"
, libnvmf_tid_str(e->tid))
;
1064 ctrl_remove(e);
1065 return 0;
1066 }
1067
1068 if (e->giveup_at_usec &&
1069 sd_event_now(ctx.event, CLOCK_BOOTTIME7, &now) >= 0 &&
1070 now >= e->giveup_at_usec) {
1071 log_warn("%s - giving up after repeated failures",dmn_log_msg(DMN_LOG_WARN, "%s - giving up after repeated failures"
, libnvmf_tid_str(e->tid))
1072 libnvmf_tid_str(e->tid))dmn_log_msg(DMN_LOG_WARN, "%s - giving up after repeated failures"
, libnvmf_tid_str(e->tid))
;
1073 if (e->is_dc) {
1074 inventory_forget_dc(ctx.inventory, e->tid);
1075 refresh_desired(); // giving up releases nothing
1076 }
1077 ctrl_remove(e);
1078 return 0;
1079 }
1080
1081 r = restart_or_start(e);
1082 if (r < 0) {
1083 log_err("%s - retry failed: %s",dmn_log_msg(DMN_LOG_ERR, "%s - retry failed: %s", libnvmf_tid_str
(e->tid), strerror(-r))
1084 libnvmf_tid_str(e->tid), strerror(-r))dmn_log_msg(DMN_LOG_ERR, "%s - retry failed: %s", libnvmf_tid_str
(e->tid), strerror(-r))
;
1085 schedule_retry(e);
1086 }
1087 return 0;
1088}
1089
1090static int epcsd_poll_timeout(sd_event_source *src,
1091 uint64_t usec __attribute__((unused)),
1092 void *user_data)
1093{
1094 struct active_ctrl *e = user_data;
1095 int r;
1096
1097 sd_event_source_unref(src);
1098 e->epcsd_poll_timer = NULL((void*)0);
1099
1100 if (!inventory_is_desired(ctx.inventory, e->tid)) {
1101 log_info("%s - no longer desired, dropping",dmn_log_msg(DMN_LOG_INFO, "%s - no longer desired, dropping",
libnvmf_tid_str(e->tid))
1102 libnvmf_tid_str(e->tid))dmn_log_msg(DMN_LOG_INFO, "%s - no longer desired, dropping",
libnvmf_tid_str(e->tid))
;
1103 ctrl_remove(e);
1104 return 0;
1105 }
1106
1107 log_dbg("%s - EPCSD poll: reconnecting to re-check",dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s - EPCSD poll: reconnecting to re-check"
, __func__, libnvmf_tid_str(e->tid))
1108 libnvmf_tid_str(e->tid))dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s - EPCSD poll: reconnecting to re-check"
, __func__, libnvmf_tid_str(e->tid))
;
1109 r = restart_or_start(e);
1110 if (r < 0) {
1111 log_err("%s - EPCSD poll reconnect failed: %s",dmn_log_msg(DMN_LOG_ERR, "%s - EPCSD poll reconnect failed: %s"
, libnvmf_tid_str(e->tid), strerror(-r))
1112 libnvmf_tid_str(e->tid), strerror(-r))dmn_log_msg(DMN_LOG_ERR, "%s - EPCSD poll reconnect failed: %s"
, libnvmf_tid_str(e->tid), strerror(-r))
;
1113 schedule_retry(e);
1114 }
1115 return 0;
1116}
1117
1118/*
1119 * Disconnect a DC whose effective EPCSD is 0 and arm a long poll timer to
1120 * reconnect and re-check it later. Never retried with the failure
1121 * backoff/give-up path — EPCSD=0 is an expected, stable outcome, not a
1122 * connect failure.
1123 */
1124static void epcsd_park(struct active_ctrl *e)
1125{
1126 uint64_t now, interval;
1127
1128 if (e->epcsd_poll_timer)
1129 return; // already parked
1130
1131 unit_stop(ctx.umgr, e->unit_name);
1132
1133 if (sd_event_now(ctx.event, CLOCK_BOOTTIME7, &now) < 0)
1134 return;
1135
1136 interval = (uint64_t)ctx.cfg->epcsd_poll_interval_minutes *
1137 60 * UINT64_C(1000000)1000000UL;
1138
1139 if (sd_event_add_time(ctx.event, &e->epcsd_poll_timer, CLOCK_BOOTTIME7,
1140 now + interval, 0, epcsd_poll_timeout, e) < 0) {
1141 log_warn("%s - failed to arm EPCSD poll timer",dmn_log_msg(DMN_LOG_WARN, "%s - failed to arm EPCSD poll timer"
, libnvmf_tid_str(e->tid))
1142 libnvmf_tid_str(e->tid))dmn_log_msg(DMN_LOG_WARN, "%s - failed to arm EPCSD poll timer"
, libnvmf_tid_str(e->tid))
;
1143 return;
1144 }
1145
1146 log_info("%s - EPCSD=0, disconnecting; re-checking in %u min",dmn_log_msg(DMN_LOG_INFO, "%s - EPCSD=0, disconnecting; re-checking in %u min"
, libnvmf_tid_str(e->tid), ctx.cfg->epcsd_poll_interval_minutes
)
1147 libnvmf_tid_str(e->tid), ctx.cfg->epcsd_poll_interval_minutes)dmn_log_msg(DMN_LOG_INFO, "%s - EPCSD=0, disconnecting; re-checking in %u min"
, libnvmf_tid_str(e->tid), ctx.cfg->epcsd_poll_interval_minutes
)
;
1148}
1149
1150// Periodic FC kickstart (opt-in via fc-kickstart-interval-minutes).
1151static int fc_kickstart_timeout(sd_event_source *src,
1152 uint64_t usec __attribute__((unused)),
1153 void *user_data __attribute__((unused)))
1154{
1155 uint64_t now;
1156
1157 fc_kickstart();
1158
1159 if (sd_event_now(ctx.event, CLOCK_BOOTTIME7, &now) >= 0) {
1160 uint64_t interval =
1161 (uint64_t)ctx.cfg->fc_kickstart_interval_minutes *
1162 60 * UINT64_C(1000000)1000000UL;
1163
1164 sd_event_source_set_time(src, now + interval);
1165 sd_event_source_set_enabled(src, SD_EVENT_ONESHOT);
1166 }
1167 return 0;
1168}
1169
1170/*
1171 * Link a newly connected DC to its device and fetch its DLP. Both the
1172 * device's add event and the unit's job completion call this, in either
1173 * order. Only the first call does the work.
1174 */
1175static void dc_connected(struct active_ctrl *e, const char *devname)
1176{
1177 if (e->devname)
1178 return;
1179
1180 e->devname = strdup(devname);
1181
1182 /*
1183 * DLP entries inherit host-side fields from the DC's TID. Use the
1184 * candidate TID, not one read from sysfs: sysfs reports the source
1185 * address the kernel selected, which the configuration did not ask
1186 * for.
1187 */
1188 dlp_fetch(e);
1189}
1190
1191static void on_job_done(const char *unit_name, bool_Bool success,
1192 void *user_data __attribute__((unused)))
1193{
1194 struct active_ctrl *e;
1195
1196 e = ctrl_find_by_unit(unit_name);
1197 if (!e) {
1198 if (!success)
1199 log_warn("unit %s failed (untracked)", unit_name)dmn_log_msg(DMN_LOG_WARN, "unit %s failed (untracked)", unit_name
)
;
1200 return;
1201 }
1202
1203 if (success) {
1204 e->attempts = 0;
1205 e->giveup_at_usec = 0;
1206
1207 /*
1208 * The device's add event is soaked for about a second and
1209 * can be processed before ExecStartPost= wrote the state
1210 * file. The job completes only after ExecStartPost=.
1211 */
1212 if (e->is_dc) {
1213 __cleanup_free__attribute__((cleanup(shr_freep))) char *devname =
1214 unit_read_devid(unit_name);
1215
1216 if (devname)
1217 dc_connected(e, devname);
1218 }
1219 return;
1220 }
1221
1222 log_warn("%s - connection unit failed", libnvmf_tid_str(e->tid))dmn_log_msg(DMN_LOG_WARN, "%s - connection unit failed", libnvmf_tid_str
(e->tid))
;
1223 if (schedule_retry(e) < 0)
1224 log_err("%s - failed to schedule retry",dmn_log_msg(DMN_LOG_ERR, "%s - failed to schedule retry", libnvmf_tid_str
(e->tid))
1225 libnvmf_tid_str(e->tid))dmn_log_msg(DMN_LOG_ERR, "%s - failed to schedule retry", libnvmf_tid_str
(e->tid))
;
1226}
1227
1228static void on_dc_add(const char *devname,
1229 void *user_data __attribute__((unused)))
1230{
1231 struct active_ctrl *e;
1232 char *unit_name;
1233
1234 // Only a DC that nvme-discoverd connected has a state file.
1235 unit_name = state_read_unit(devname);
1236 if (!unit_name)
1237 return;
1238
1239 e = ctrl_find_by_unit(unit_name);
1240 free(unit_name);
1241 if (e)
1242 dc_connected(e, devname);
1243}
1244
1245/*
1246 * A DC that nobody owns, e.g. from a manual "nvme discover --persistent".
1247 * Without the legacy udev rules, nothing else handles its events. Run
1248 * connect-all for it, as those rules do. The connections it makes stay
1249 * unowned, like the DC.
1250 */
1251static void dc_changed_unowned(const char *devname, sd_device *dev)
1252{
1253 __cleanup_free__attribute__((cleanup(shr_freep))) char *owner = NULL((void*)0);
1254 struct dc_addr addr = { };
1255 int r;
1256
1257 r = libnvmf_registry_retrieve(ctx.nvme_ctx, devname, "owner", &owner);
1258 if (r < 0 && r != -ENOENT2) {
1259 log_warn("%s - failed to check registry owner: %s",dmn_log_msg(DMN_LOG_WARN, "%s - failed to check registry owner: %s"
, devname, strerror(-r))
1260 devname, strerror(-r))dmn_log_msg(DMN_LOG_WARN, "%s - failed to check registry owner: %s"
, devname, strerror(-r))
;
1261 return;
1262 }
1263 if (owner) {
1264 log_dbg("%s - discovery log changed, owned by '%s', skipping",dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s - discovery log changed, owned by '%s', skipping"
, __func__, devname, owner)
1265 devname, owner)dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s - discovery log changed, owned by '%s', skipping"
, __func__, devname, owner)
;
1266 return;
1267 }
1268
1269 sd_device_get_property_value(dev, "NVME_TRTYPE", &addr.transport);
1270 sd_device_get_property_value(dev, "NVME_TRADDR", &addr.traddr);
1271 sd_device_get_property_value(dev, "NVME_TRSVCID", &addr.trsvcid);
1272 sd_device_get_property_value(dev, "NVME_HOST_TRADDR",
1273 &addr.host_traddr);
1274 sd_device_get_property_value(dev, "NVME_HOST_IFACE",
1275 &addr.host_iface);
1276
1277 log_info("%s - discovery log changed on an unowned DC, running connect-all",dmn_log_msg(DMN_LOG_INFO, "%s - discovery log changed on an unowned DC, running connect-all"
, devname)
1278 devname)dmn_log_msg(DMN_LOG_INFO, "%s - discovery log changed on an unowned DC, running connect-all"
, devname)
;
1279 r = unit_start_connect_all(ctx.umgr, devname, &addr);
1280 if (r < 0)
1281 log_err("%s - failed to start connect-all: %s",dmn_log_msg(DMN_LOG_ERR, "%s - failed to start connect-all: %s"
, devname, strerror(-r))
1282 devname, strerror(-r))dmn_log_msg(DMN_LOG_ERR, "%s - failed to start connect-all: %s"
, devname, strerror(-r))
;
1283}
1284
1285static void on_dc_changed(const char *devname, sd_device *dev,
1286 void *user_data __attribute__((unused)))
1287{
1288 struct active_ctrl *e;
1289
1290 e = ctrl_find_by_devname(devname);
1291 if (!e) {
1292 dc_changed_unowned(devname, dev);
1293 return;
1294 }
1295 if (!e->tid) {
1296 log_warn("%s - dc_changed for untracked device", devname)dmn_log_msg(DMN_LOG_WARN, "%s - dc_changed for untracked device"
, devname)
;
1297 return;
1298 }
1299
1300 log_dbg("%s | %s: discovery log changed, re-fetching",dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s | %s: discovery log changed, re-fetching"
, __func__, libnvmf_tid_str(e->tid), devname)
1301 libnvmf_tid_str(e->tid), devname)dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s | %s: discovery log changed, re-fetching"
, __func__, libnvmf_tid_str(e->tid), devname)
;
1302 dlp_fetch(e);
1303}
1304
1305static void on_ioc_add(const char *devname,
1306 void *user_data __attribute__((unused)))
1307{
1308 struct active_ctrl *e;
1309 char *unit_name;
1310
1311 unit_name = state_read_unit(devname);
1312 if (!unit_name)
1313 return;
1314
1315 e = ctrl_find_by_unit(unit_name);
1316 if (e && !e->devname)
1317 e->devname = strdup(devname);
1318 free(unit_name);
1319}
1320
1321static bool_Bool sysfs_ctrl_deleting(const char *devname)
1322{
1323 sd_device *dev = NULL((void*)0);
1324 const char *state = NULL((void*)0);
1325 char syspath[256];
1326 bool_Bool deleting;
1327
1328 snprintf(syspath, sizeof(syspath), SYSFS_NVME_DIR"/sys/class/nvme" "/%s", devname);
1329 if (sd_device_new_from_syspath(&dev, syspath) < 0)
1330 return true1;
1331
1332 sd_device_get_sysattr_value(dev, "state", &state);
1333 deleting = !state || !strncmp(state, "deleting", 8);
1334 sd_device_unref(dev);
1335
1336 return deleting;
1337}
1338
1339static void on_nvme_remove(const char *devname,
1340 void *user_data __attribute__((unused)))
1341{
1342 struct active_ctrl *e;
1343 bool_Bool is_fc;
1344
1345 /*
1346 * The kernel reuses a device name as soon as it is free. A "remove"
1347 * can arrive after a new device took the name, and the new device
1348 * must be kept. The kernel sends "remove" before it deletes the
1349 * sysfs directory, so a device that is being deleted is not new.
1350 */
1351 if (state_ctrl_present(devname) && !sysfs_ctrl_deleting(devname)) {
1352 log_dbg("%s - remove for an earlier device of this name, ignored",dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s - remove for an earlier device of this name, ignored"
, __func__, devname)
1353 devname)dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "%s - remove for an earlier device of this name, ignored"
, __func__, devname)
;
1354 return;
1355 }
1356
1357 e = ctrl_find_by_devname(devname);
1358 state_remove_ctrl(devname);
1359 if (!e)
1360 return; // might be a controller we didn't start — ignore
1361
1362 state_remove_devid(e->unit_name); // ExecStopPost usually beat us to it
1363
1364 free(e->devname);
1365 e->devname = NULL((void*)0);
1366
1367 /*
1368 * Both checks must pass before reconnecting: a matching exclusion
1369 * entry wins even over a still-desired controller — it is the
1370 * administrator's explicit override.
1371 */
1372 if (!should_connect(NULL((void*)0), e->tid, devname)) {
1373 unit_stop(ctx.umgr, e->unit_name);
1374 ctrl_remove(e);
1375 return;
1376 }
1377
1378 /*
1379 * Checked before "not desired": this removal is discoverd's own
1380 * doing, and the poll timer owns what happens next. Checking
1381 * "desired" first can call ctrl_remove(), which frees the timer
1382 * epcsd_park() armed moments ago. The timer re-checks "desired"
1383 * itself when it fires, so nothing is skipped by deferring.
1384 */
1385 if (e->epcsd_poll_timer)
1386 return;
1387
1388 if (!inventory_is_desired(ctx.inventory, e->tid)) {
1389 log_info("%s | %s - removed, not desired, dropping",dmn_log_msg(DMN_LOG_INFO, "%s | %s - removed, not desired, dropping"
, libnvmf_tid_str(e->tid), devname)
1390 libnvmf_tid_str(e->tid), devname)dmn_log_msg(DMN_LOG_INFO, "%s | %s - removed, not desired, dropping"
, libnvmf_tid_str(e->tid), devname)
;
1391 ctrl_remove(e);
1392 return;
1393 }
1394
1395 log_info("%s | %s - removed but still desired, reconnecting",dmn_log_msg(DMN_LOG_INFO, "%s | %s - removed but still desired, reconnecting"
, libnvmf_tid_str(e->tid), devname)
1396 libnvmf_tid_str(e->tid), devname)dmn_log_msg(DMN_LOG_INFO, "%s | %s - removed but still desired, reconnecting"
, libnvmf_tid_str(e->tid), devname)
;
1397
1398 is_fc = shr_streq0(libnvmf_tid_get_transport(e->tid), "fc");
1399
1400 if (is_fc) {
1401 // FC: stop old unit, re-issue kickstart.
1402 unit_stop(ctx.umgr, e->unit_name);
1403 ctrl_remove(e);
1404 fc_kickstart();
1405 } else if (restart_or_start(e) < 0) {
1406 if (schedule_retry(e) < 0)
1407 log_err("%s - failed to schedule retry",dmn_log_msg(DMN_LOG_ERR, "%s - failed to schedule retry", libnvmf_tid_str
(e->tid))
1408 libnvmf_tid_str(e->tid))dmn_log_msg(DMN_LOG_ERR, "%s - failed to schedule retry", libnvmf_tid_str
(e->tid))
;
1409 }
1410}
1411
1412static void on_fc_discovery(const struct libnvmf_tid *t,
1413 void *user_data __attribute__((unused)))
1414{
1415 __cleanup_tid__attribute__((cleanup(cleanup_tid))) struct libnvmf_tid *tid = libnvmf_tid_dup(t);
1416
1417 // The uevent names no host: connect as the default host.
1418 if (!tid ||
1419 tid_set_default_host_if_unset(tid, ctx.hostnqn, ctx.hostid) < 0)
1420 return;
1421
1422 /*
1423 * fc_monitor_handler() is the only producer of fc_discovery
1424 * callbacks, and the kernel only fires that uevent for an FC
1425 * remote port advertising FC_PORT_ROLE_NVME_DISCOVERY - so tid is
1426 * always a DC here, never an IOC. Connect as a DC; we fetch its
1427 * DLP (and discover any IOCs behind it) once the device appears.
1428 */
1429 inventory_add_discovered_dc(ctx.inventory, tid);
1430 if (should_connect(NULL((void*)0), tid, NULL((void*)0)))
1431 start_ctrl(tid, true1, NULL((void*)0));
1432}
1433
1434/*
1435 * Whether the kernel accepts a discovery connect to a DC's own NQN (TP8013).
1436 * On any error, answer no: the well-known discovery NQN always works.
1437 */
1438static bool_Bool kernel_supports_discovery_nqn(void)
1439{
1440 bool_Bool supported;
1441 int r;
1442
1443 r = libnvmf_kernel_option_supported(ctx.nvme_ctx, "discovery",
1444 &supported);
1445 if (r < 0) {
1446 log_dbg("kernel fabrics options: %s", libnvme_strerror(-r))dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "kernel fabrics options: %s"
, __func__, libnvme_strerror(-r))
;
1447 return false0;
1448 }
1449
1450 return supported;
1451}
1452
1453/*
1454 * mDNS found a DC. Connect with its advertised NQN if the kernel allows
1455 * it, else with the well-known discovery NQN. The DLP gives the real NQN
1456 * either way. host_iface is only valid for tcp. A link-local traddr gets
1457 * the interface as its scope, because rdma has no host_iface.
1458 */
1459static void on_mdns_add(const char *traddr, const char *trsvcid,
1460 const char *transport, const char *nqn,
1461 const char *ifname,
1462 int ifindex __attribute__((unused)),
1463 void *user_data __attribute__((unused)))
1464{
1465 const char *subsysnqn = NVME_DISC_SUBSYS_NAME"nqn.2014-08.org.nvmexpress.discovery";
1466 __cleanup_tid__attribute__((cleanup(cleanup_tid))) struct libnvmf_tid *tid = NULL((void*)0);
1467 bool_Bool is_tcp = shr_streq0(transport, "tcp");
1468 __cleanup_free__attribute__((cleanup(shr_freep))) char *scoped = NULL((void*)0);
1469
1470 if (nqn && kernel_supports_discovery_nqn())
1471 subsysnqn = nqn;
1472
1473 scoped = tid_scope_link_local(traddr, ifname);
1474 if (!scoped)
1475 return;
1476
1477 tid = tid_new(transport, scoped, trsvcid, subsysnqn, NULL((void*)0),
1478 is_tcp ? ifname : NULL((void*)0), NULL((void*)0), NULL((void*)0), true1);
1479 if (!tid ||
1480 tid_set_default_host_if_unset(tid, ctx.hostnqn, ctx.hostid) < 0)
1481 return;
1482
1483 inventory_add_discovered_dc(ctx.inventory, tid);
1484 if (should_connect(NULL((void*)0), tid, NULL((void*)0)))
1485 start_ctrl(tid, true1, NULL((void*)0));
1486}
1487
1488/*
1489 * No service_remove callback: an mDNS announcement can disappear briefly
1490 * (cache expiry, link flap). A connected DC is managed by the same paths
1491 * as any other DC, not by its announcement.
1492 */
1493static const struct mdns_callbacks mdns_callbacks = {
1494 .service_add = on_mdns_add,
1495};
1496
1497/* Start or stop mDNS to match the zeroconf setting. */
1498static void apply_zeroconf(void)
1499{
1500 int r;
1501
1502 if (ctx.cfg->zeroconf && !ctx.mdns) {
1503 r = mdns_start(ctx.event, &mdns_callbacks, NULL((void*)0), &ctx.mdns);
1504 if (r == -ENOSYS38)
1505 log_warn("mdns: zeroconf is enabled, but nvme-discoverd was built without mDNS support")dmn_log_msg(DMN_LOG_WARN, "mdns: zeroconf is enabled, but nvme-discoverd was built without mDNS support"
)
;
1506 else if (r == -EOPNOTSUPP95)
1507 log_warn("mdns: zeroconf is enabled, but systemd-resolved has no BrowseServices (needs >= v258)")dmn_log_msg(DMN_LOG_WARN, "mdns: zeroconf is enabled, but systemd-resolved has no BrowseServices (needs >= v258)"
)
;
1508 else if (r < 0)
1509 log_warn("mdns: zeroconf is enabled, but mDNS cannot start: %s",dmn_log_msg(DMN_LOG_WARN, "mdns: zeroconf is enabled, but mDNS cannot start: %s"
, strerror(-r))
1510 strerror(-r))dmn_log_msg(DMN_LOG_WARN, "mdns: zeroconf is enabled, but mDNS cannot start: %s"
, strerror(-r))
;
1511 } else if (!ctx.cfg->zeroconf && ctx.mdns) {
1512 mdns_stop(ctx.mdns);
1513 ctx.mdns = NULL((void*)0);
1514 }
1515}
1516
1517static struct libnvmf_tid *sysfs_read_tid(const char *devname, bool_Bool *is_dc,
1518 bool_Bool *live)
1519{
1520 sd_device *dev = NULL((void*)0);
1521 const char *state = NULL((void*)0);
1522 char syspath[256];
1523 struct libnvmf_tid *t;
1524
1525 if (is_dc)
1526 *is_dc = false0;
1527 *live = false0;
1528
1529 snprintf(syspath, sizeof(syspath), "/sys/class/nvme/%s", devname);
1530 if (sd_device_new_from_syspath(&dev, syspath) < 0)
1531 return NULL((void*)0);
1532
1533 t = tid_from_sysfs(dev, is_dc);
1534 sd_device_get_sysattr_value(dev, "state", &state);
1535 *live = shr_streq0(state, "live");
1536 sd_device_unref(dev);
1537 return t;
1538}
1539
1540/*
1541 * Is @devname a connection that satisfies @tid? A device name alone does not
1542 * identify a controller, because the kernel reuses nvmeN names.
1543 */
1544static bool_Bool devname_matches_tid(const char *devname,
1545 const struct libnvmf_tid *tid)
1546{
1547 __cleanup_tid__attribute__((cleanup(cleanup_tid))) struct libnvmf_tid *existing = NULL((void*)0);
1548 struct ifaddrs *iface_list = NULL((void*)0);
1549 bool_Bool is_dc, live, match;
1550
1551 existing = sysfs_read_tid(devname, &is_dc, &live);
1552 if (!existing)
1553 return false0;
1554
1555 if (getifaddrs(&iface_list) < 0) {
1556 log_warn("getifaddrs: %s", strerror(errno))dmn_log_msg(DMN_LOG_WARN, "getifaddrs: %s", strerror((*__errno_location
())))
;
1557 iface_list = NULL((void*)0);
1558 }
1559
1560 match = tid_matches_existing(tid, existing, is_dc, live, iface_list);
1561 freeifaddrs(iface_list);
1562
1563 return match;
1564}
1565
1566static void conn_scan_free(struct conn_scan *scan)
1567{
1568 size_t i;
1569
1570 for (i = 0; i < scan->ctrls.len; i++)
1571 scanned_ctrl_free(scan->ctrls.items[i]);
1572 scanned_ctrl_list_free(&scan->ctrls);
1573 freeifaddrs(scan->iface_list);
1574 memset(scan, 0, sizeof(*scan));
1575}
1576
1577/*
1578 * Read every connected controller out of sysfs and snapshot the host's
1579 * interface addresses. A controller whose TID cannot be built is left out:
1580 * it cannot be matched against anyway. On failure the scan is left short
1581 * rather than failed, which costs a missed match, not a crash.
1582 */
1583static void conn_scan_load(struct conn_scan *scan)
1584{
1585 DIR *d;
1586 struct dirent *ent;
1587
1588 memset(scan, 0, sizeof(*scan));
1589
1590 if (getifaddrs(&scan->iface_list) < 0) {
1591 log_warn("getifaddrs: %s", strerror(errno))dmn_log_msg(DMN_LOG_WARN, "getifaddrs: %s", strerror((*__errno_location
())))
;
1592 scan->iface_list = NULL((void*)0);
1593 }
1594
1595 d = opendir("/sys/class/nvme");
1596 if (!d) {
1597 if (errno(*__errno_location ()) != ENOENT2)
1598 log_warn("opendir /sys/class/nvme: %s",dmn_log_msg(DMN_LOG_WARN, "opendir /sys/class/nvme: %s", strerror
((*__errno_location ())))
1599 strerror(errno))dmn_log_msg(DMN_LOG_WARN, "opendir /sys/class/nvme: %s", strerror
((*__errno_location ())))
;
1600 return;
1601 }
1602
1603 while ((ent = readdir(d))) {
1604 struct scanned_ctrl *sc;
1605
1606 if (ent->d_name[0] == '.')
1607 continue;
1608
1609 sc = calloc(1, sizeof(*sc));
1610 if (!sc) {
1611 log_warn("connection scan: out of memory")dmn_log_msg(DMN_LOG_WARN, "connection scan: out of memory");
1612 break;
1613 }
1614
1615 sc->tid = sysfs_read_tid(ent->d_name, &sc->is_dc, &sc->live);
1616 sc->devname = shr_xstrdup(ent->d_name);
1617 if (!sc->tid || !sc->devname) {
1618 scanned_ctrl_free(sc);
1619 continue;
1620 }
1621
1622 if (scanned_ctrl_list_append(&scan->ctrls, sc) < 0) {
1623 log_warn("connection scan: out of memory")dmn_log_msg(DMN_LOG_WARN, "connection scan: out of memory");
1624 scanned_ctrl_free(sc);
1625 break;
1626 }
1627 }
1628 closedir(d);
1629}
1630
1631/*
1632 * Find the device name (e.g. "nvme3") of a connected controller that
1633 * satisfies @tid, using the snapshot rather than libnvme's topology tree
1634 * (see should_connect()'s comment for why that tree can't be relied on
1635 * here). Returns NULL if nothing in the snapshot matches. The returned
1636 * string belongs to the scan.
1637 */
1638static const char *find_devname_for_tid(const struct conn_scan *scan,
1639 const struct libnvmf_tid *tid)
1640{
1641 size_t i;
1642
1643 for (i = 0; i < scan->ctrls.len; i++) {
1644 const struct scanned_ctrl *sc = scan->ctrls.items[i];
1645
1646 if (tid_matches_existing(tid, sc->tid, sc->is_dc, sc->live,
1647 scan->iface_list))
1648 return sc->devname;
1649 }
1650 return NULL((void*)0);
1651}
1652
1653static void connect_desired(void)
1654{
1655 __cleanup_conn_scan__attribute__((cleanup(conn_scan_free))) struct conn_scan scan = { 0 };
1656 struct libnvmf_tid **dcs, **iocs;
1657 int i;
1658
1659 conn_scan_load(&scan);
1660
1661 dcs = inventory_desired_dcs(ctx.inventory);
1662 if (dcs) {
1663 for (i = 0; dcs[i]; i++) {
1664 if (should_connect(&scan, dcs[i], NULL((void*)0)))
1665 start_ctrl(dcs[i], true1, NULL((void*)0));
1666 tid_free(dcs[i]);
1667 }
1668 free(dcs);
1669 }
1670
1671 iocs = inventory_desired_iocs(ctx.inventory);
1672 if (iocs) {
1673 for (i = 0; iocs[i]; i++) {
1674 if (should_connect(&scan, iocs[i], NULL((void*)0)))
1675 start_ctrl(iocs[i], false0, NULL((void*)0));
1676 tid_free(iocs[i]);
1677 }
1678 free(iocs);
1679 }
1680}
1681
1682/*
1683 * Apply the effective log level to both discoverd and its in-process libnvme
1684 * context. A command-line --debug (ctx.force_debug) forces DEBUG and
1685 * overrides the config; otherwise the configured debug-level (default INFO)
1686 * is used. Called at startup after config_load() and again after a SIGHUP
1687 * reload, since the libnvme context outlives any single config.
1688 */
1689/*
1690 * A name lookup of the fabrics configuration ended. Connect what it added,
1691 * and run the release pass that waited for it.
1692 */
1693static void on_config_lookup(bool_Bool added,
1694 void *user_data __attribute__((unused)))
1695{
1696 if (added)
1697 connect_desired();
1698 schedule_release(false0);
1699}
1700
1701static void apply_log_level(void)
1702{
1703 int level = log_level();
1704
1705 dmn_log_set_level(level);
1706 libnvme_set_logging_level(ctx.nvme_ctx, level, false0, false0);
1707}
1708
1709static void reload_config(void *user_data __attribute__((unused)))
1710{
1711 struct discoverd_config *new_cfg;
1712 struct libnvmf_config *new_fabrics_cfg;
1713
1714 new_cfg = config_load(ctx.conf_path);
1715 if (!new_cfg) {
1716 log_err("failed to reload config")dmn_log_msg(DMN_LOG_ERR, "failed to reload config");
1717 return;
1718 }
1719 config_free(ctx.cfg);
1720 ctx.cfg = new_cfg;
1721 apply_log_level();
1722
1723 if (libnvmf_config_read(ctx.nvme_ctx, NULL((void*)0), &new_fabrics_cfg) == 0) {
1724 libnvmf_config_free(ctx.fabrics_cfg);
1725 ctx.fabrics_cfg = new_fabrics_cfg;
1726 } else {
1727 log_err("failed to reload fabrics config, keeping last-good")dmn_log_msg(DMN_LOG_ERR, "failed to reload fabrics config, keeping last-good"
)
;
1728 }
1729 inventory_load_config(ctx.inventory, &ctx);
1730
1731 /*
1732 * Connect any newly added desired controllers. Release those the
1733 * configuration or the exclusion list removed, without disconnecting
1734 * them.
1735 */
1736 connect_desired();
1737 schedule_release(true1);
1738 apply_zeroconf();
1739}
1740
1741/*
1742 * Resolve the identity a connect without --hostnqn uses. Must run before
1743 * anything scans a controller into the libnvme tree:
1744 * libnvmf_host_get_ids() prefers a host already in the tree.
1745 */
1746static int resolve_default_host(void)
1747{
1748 int r;
1749
1750 r = libnvmf_host_get_ids(ctx.nvme_ctx, NULL((void*)0), NULL((void*)0),
1751 &ctx.hostnqn, &ctx.hostid);
1752 if (r < 0)
1753 return r;
1754
1755 log_dbg("default host identity: %s, %s", ctx.hostnqn, ctx.hostid)dmn_log_msg(DMN_LOG_DEBUG, "%s() - " "default host identity: %s, %s"
, __func__, ctx.hostnqn, ctx.hostid)
;
1756
1757 return 0;
1758}
1759
1760int main(int argc, char **argv)
1761{
1762 static const struct events_callbacks callbacks = {
1763 .dc_add = on_dc_add,
1764 .dc_changed = on_dc_changed,
1765 .ioc_add = on_ioc_add,
1766 .nvme_remove = on_nvme_remove,
1767 .fc_discovery = on_fc_discovery,
1768 };
1769 static const struct option long_opts[] = {
1770 { "config", required_argument1, NULL((void*)0), 'c' },
1771 { "nvme-path", required_argument1, NULL((void*)0), 'N' },
1772 { "debug", no_argument0, NULL((void*)0), 'd' },
1773 { "help", no_argument0, NULL((void*)0), 'h' },
1774 { NULL((void*)0), 0, NULL((void*)0), 0 },
1775 };
1776 const char *nvme_path = NULL((void*)0), *config_path = NULL((void*)0);
1777 char *nvme_path_abs = NULL((void*)0), *config_path_abs = NULL((void*)0);
1778 bool_Bool debug = false0;
1779 int r, c;
1780
1781 while ((c = getopt_long(argc, argv, "c:dh", long_opts, NULL((void*)0))) != -1) {
1782 switch (c) {
1783 case 'c':
1784 config_path = optarg;
1785 break;
1786 case 'N':
1787 nvme_path = optarg;
1788 break;
1789 case 'd':
1790 debug = true1;
1791 break;
1792 case 'h':
1793 printf("Usage: %s [OPTIONS]\n"
1794 "\n"
1795 "All options are optional; specify one only to override its default.\n"
1796 " --config FILE, -c FILE discoverd configuration file\n"
1797 " (default: " DISCOVERD_CONF_PATH"/usr/local/etc" "/nvme/nvme-discoverd.conf" ")\n"
1798 " --nvme-path PATH nvme binary the connection units exec\n"
1799 " (default: <sbindir>/nvme)\n"
1800 " --debug, -d enable debug logging (journal + libnvme)\n"
1801 " --help, -h show this help and exit\n",
1802 argv[0]);
1803 return 0;
1804 default:
1805 fprintf(stderrstderr, "Try '%s --help'.\n", argv[0]);
1806 return 1;
1807 }
1808 }
1809
1810 if (debug)
1811 dmn_log_set_level(DMN_LOG_DEBUG);
1812 ctx.force_debug = debug;
1813
1814 /*
1815 * The nvme path is baked into each transient unit's ExecStart=, which
1816 * systemd requires to be absolute. Canonicalize it (resolving a
1817 * relative path against the current directory) so the daemon can be
1818 * launched as, e.g., --nvme-path ./.build/nvme. realpath() also
1819 * confirms the binary exists, failing fast on a typo.
1820 */
1821 if (nvme_path) {
1822 nvme_path_abs = realpath(nvme_path, NULL((void*)0));
1823 if (!nvme_path_abs) {
1824 fprintf(stderrstderr, "--nvme-path: cannot resolve '%s': %s\n",
1825 nvme_path, strerror(errno(*__errno_location ())));
1826 return 1;
1827 }
1828 }
1829
1830 if (config_path) {
1831 config_path_abs = realpath(config_path, NULL((void*)0));
1832 if (!config_path_abs) {
1833 fprintf(stderrstderr, "--config: cannot resolve '%s': %s\n",
1834 config_path, strerror(errno(*__errno_location ())));
1835 return 1;
1836 }
1837 }
1838 ctx.conf_path = config_path_abs ? config_path_abs : DISCOVERD_CONF_PATH"/usr/local/etc" "/nvme/nvme-discoverd.conf";
1839
1840 r = sd_event_default(&ctx.event);
1841 if (r < 0) {
1842 log_err("sd_event_default: %s", strerror(-r))dmn_log_msg(DMN_LOG_ERR, "sd_event_default: %s", strerror(-r)
)
;
1843 return 1;
1844 }
1845
1846 r = sd_bus_open_system(&ctx.bus);
1847 if (r < 0) {
1848 log_err("sd_bus_open_system: %s", strerror(-r))dmn_log_msg(DMN_LOG_ERR, "sd_bus_open_system: %s", strerror(-
r))
;
1849 return 1;
1850 }
1851
1852 r = sd_bus_attach_event(ctx.bus, ctx.event, SD_EVENT_PRIORITY_NORMAL);
1853 if (r < 0) {
1854 log_err("sd_bus_attach_event: %s", strerror(-r))dmn_log_msg(DMN_LOG_ERR, "sd_bus_attach_event: %s", strerror(
-r))
;
1855 return 1;
1856 }
1857
1858 r = state_init();
1859 if (r < 0) {
1860 log_err("state_init: %s", strerror(-r))dmn_log_msg(DMN_LOG_ERR, "state_init: %s", strerror(-r));
1861 return 1;
1862 }
1863 state_gc();
1864
1865 ctx.nvme_ctx = libnvme_create_global_ctx();
1866 if (!ctx.nvme_ctx) {
1867 log_err("libnvme_create_global_ctx: failed")dmn_log_msg(DMN_LOG_ERR, "libnvme_create_global_ctx: failed");
1868 return 1;
1869 }
1870 libnvme_set_logging_level(ctx.nvme_ctx,
1871 debug ? LIBNVME_LOG_DEBUG : LIBNVME_LOG_ERR,
1872 false0, false0);
1873
1874 ctx.cfg = config_load(ctx.conf_path);
1875 if (!ctx.cfg) {
1876 log_err("config_load: failed")dmn_log_msg(DMN_LOG_ERR, "config_load: failed");
1877 return 1;
1878 }
1879 apply_log_level();
1880
1881 r = libnvmf_config_read(ctx.nvme_ctx, NULL((void*)0), &ctx.fabrics_cfg);
1882 if (r < 0) {
1883 log_err("libnvmf_config_read: %s", strerror(-r))dmn_log_msg(DMN_LOG_ERR, "libnvmf_config_read: %s", strerror(
-r))
;
1884 return 1;
1885 }
1886
1887 r = resolve_default_host();
1888 if (r < 0) {
1889 log_err("failed to resolve the host identity: %s",dmn_log_msg(DMN_LOG_ERR, "failed to resolve the host identity: %s"
, strerror(-r))
1890 strerror(-r))dmn_log_msg(DMN_LOG_ERR, "failed to resolve the host identity: %s"
, strerror(-r))
;
1891 return 1;
1892 }
1893
1894 r = dmn_resolver_new(ctx.event, RESOLVE_WORKERS8, &ctx.resolver);
1895 if (r < 0) {
1896 log_err("failed to create the resolver: %s", strerror(-r))dmn_log_msg(DMN_LOG_ERR, "failed to create the resolver: %s",
strerror(-r))
;
1897 return 1;
1898 }
1899
1900 r = dmn_work_pool_new(ctx.event, DLP_WORKERS4, &ctx.dlp_pool);
1901 if (r < 0) {
1902 log_err("failed to create the DLP workers: %s", strerror(-r))dmn_log_msg(DMN_LOG_ERR, "failed to create the DLP workers: %s"
, strerror(-r))
;
1903 return 1;
1904 }
1905
1906 ctx.inventory = inventory_new();
1907 if (!ctx.inventory)
1908 return 1;
1909 inventory_set_lookup_callback(ctx.inventory, on_config_lookup, NULL((void*)0));
1910
1911 if (ctx.cfg->nbft)
1912 inventory_load_nbft(ctx.inventory, &ctx, NBFT_SYSFS_PATH"/sys/firmware/acpi/tables");
1913 inventory_load_config(ctx.inventory, &ctx);
1914 load_saved();
1915
1916 ctx.umgr = unit_mgr_new(ctx.bus, ctx.event, on_job_done, NULL((void*)0),
1917 nvme_path_abs);
1918 if (!ctx.umgr)
1919 return 1;
1920
1921 ctx.evts = events_start(ctx.event, &callbacks, NULL((void*)0));
1922 if (!ctx.evts)
1923 return 1;
1924
1925 if (dmn_add_signal_handlers(ctx.event, reload_config, NULL((void*)0)) < 0)
1926 return 1;
1927
1928 /*
1929 * Connect the desired set. start_ctrl() adopts anything a previous
1930 * run of this daemon left connected. The release pass then decides
1931 * what the last run wanted and this one does not.
1932 */
1933 connect_desired();
1934 schedule_release(false0);
1935
1936 /*
1937 * One-shot startup FC kickstart: mimics nvmefc-boot-connections.service
1938 * (which discoverd replaces). Always issued — fc_kickstart() is a
1939 * no-op (returns 0 on ENOENT) when no FC HBA is present, so it is
1940 * harmless on non-FC hosts. Independent of the periodic-kickstart
1941 * knob below.
1942 */
1943 fc_kickstart();
1944
1945 apply_zeroconf();
1946
1947 // Periodic FC kickstart: opt-in (default 0 = disabled).
1948 if (ctx.cfg->fc_kickstart_interval_minutes > 0) {
1949 uint64_t now, interval;
1950
1951 interval = (uint64_t)ctx.cfg->fc_kickstart_interval_minutes *
1952 60 * UINT64_C(1000000)1000000UL;
1953 r = sd_event_now(ctx.event, CLOCK_BOOTTIME7, &now);
1954 if (r >= 0)
1955 r = sd_event_add_time(ctx.event, NULL((void*)0), CLOCK_BOOTTIME7,
1956 now + interval, 0,
1957 fc_kickstart_timeout, NULL((void*)0));
1958 if (r < 0)
1959 log_err("failed to arm FC kickstart timer: %s",dmn_log_msg(DMN_LOG_ERR, "failed to arm FC kickstart timer: %s"
, strerror(-r))
1960 strerror(-r))dmn_log_msg(DMN_LOG_ERR, "failed to arm FC kickstart timer: %s"
, strerror(-r))
;
1961 }
1962
1963 sd_notify(0, "READY=1");
1964
1965 r = sd_event_loop(ctx.event);
1966 if (r < 0)
1967 log_err("sd_event_loop: %s", strerror(-r))dmn_log_msg(DMN_LOG_ERR, "sd_event_loop: %s", strerror(-r));
1968
1969 mdns_stop(ctx.mdns);
1970 events_stop(ctx.evts);
1971 unit_mgr_free(ctx.umgr);
1972 free(nvme_path_abs);
1973 free(config_path_abs);
1974 inventory_free(ctx.inventory);
1975 dmn_resolver_free(ctx.resolver);
1976 dmn_work_pool_free(ctx.dlp_pool);
1977 free(ctx.hostnqn);
1978 free(ctx.hostid);
1979 config_free(ctx.cfg);
1980 libnvmf_config_free(ctx.fabrics_cfg);
1981 libnvme_free_global_ctx(ctx.nvme_ctx);
1982 sd_bus_unref(ctx.bus);
1983 sd_event_unref(ctx.event);
1984
1985 return r < 0 ? 1 : 0;
1986}