diff --git a/libnvme/libnvme/meson.build b/libnvme/libnvme/meson.build index de4111881a..80547e0525 100644 --- a/libnvme/libnvme/meson.build +++ b/libnvme/libnvme/meson.build @@ -14,6 +14,12 @@ if want_python swig_cmd = [swig, '-python', '-o', '@OUTPUT1@', '@INPUT0@'] endif + py_compat_c_args = [] + if not cc.has_header_symbol('Python.h', 'Py_NewRef', dependencies: py3_dep) + warning('Python < 3.10 detected: providing Py_NewRef compatibility shim in nvme.i') + py_compat_c_args = ['-DSWIG_COMPAT_PY_NEWREF'] + endif + pymod_swig = custom_target( 'nvme.py', input: ['nvme.i'], @@ -28,6 +34,7 @@ if want_python pynvme_clib = python3.extension_module( '_nvme', nvme_wrap_c, + c_args: py_compat_c_args, dependencies: [config_dep, ccan_dep, libnvme_dep, py3_dep], install: true, subdir: 'libnvme', diff --git a/libnvme/libnvme/nvme.i b/libnvme/libnvme/nvme.i index b2641f548d..a958060b57 100644 --- a/libnvme/libnvme/nvme.i +++ b/libnvme/libnvme/nvme.i @@ -10,6 +10,16 @@ clashes with the same macro defined in Python.h. */ #undef fallthrough + +/* WORKAROUND: Py_NewRef() was introduced in Python 3.10. SWIG >= 4.1 generates + calls to it in its runtime boilerplate. On older distributions, + meson detects the absence of Py_NewRef and passes + -DSWIG_COMPAT_PY_NEWREF so we can provide a macro shim here, + before Python.h is included. + */ +#ifdef SWIG_COMPAT_PY_NEWREF +#define Py_NewRef(obj) (Py_INCREF(obj), (obj)) +#endif %} %module(docstring="Python bindings for libnvme") nvme @@ -19,12 +29,11 @@ %allowexception; -%rename(global_ctx) libnvme_global_ctx; -%rename(host) libnvme_host; -%rename(ctrl) libnvme_ctrl; -%rename(subsystem) libnvme_subsystem; -%rename(ns) libnvme_ns; -%rename(fabrics_context) libnvmf_context; +%rename(global_ctx) libnvme_global_ctx; +%rename(host) libnvme_host; +%rename(ctrl) libnvme_ctrl; +%rename(subsystem) libnvme_subsystem; +%rename(ns) libnvme_ns; %{ #include @@ -52,10 +61,213 @@ free(val); return obj; } + + static const char *dict_get_str(PyObject *dict, const char *key) + { + PyObject *val = PyDict_GetItemString(dict, key); + + if (!val || val == Py_None) + return NULL; + return PyUnicode_AsUTF8(val); + } + + static int set_fctx_from_dict(struct libnvmf_context *fctx, PyObject *dict) + { + struct libnvme_fabrics_config *cfg; + const char *subsysnqn, *transport; + const char *hostnqn = NULL, *hostid = NULL; + const char *hostkey = NULL, *ctrlkey = NULL; + const char *keyring = NULL, *tls_key = NULL, *tls_key_identity = NULL; + bool persistent = false; + bool has_persistent = false; + Py_ssize_t pos = 0; + PyObject *key, *value; + + subsysnqn = dict_get_str(dict, "subsysnqn"); + transport = dict_get_str(dict, "transport"); + + if (!subsysnqn || !transport) { + PyErr_SetString(PyExc_KeyError, + "'subsysnqn' and 'transport' are required"); + return -1; + } + + libnvmf_context_set_connection(fctx, subsysnqn, transport, + dict_get_str(dict, "traddr"), + dict_get_str(dict, "trsvcid"), + dict_get_str(dict, "host_traddr"), + dict_get_str(dict, "host_iface")); + + cfg = libnvmf_context_get_fabrics_config(fctx); + + while (PyDict_Next(dict, &pos, &key, &value)) { + if (!PyUnicode_CompareWithASCIIString(key, "queue_size")) { + libnvme_fabrics_config_set_queue_size(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "nr_io_queues")) { + libnvme_fabrics_config_set_nr_io_queues(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "reconnect_delay")) { + libnvme_fabrics_config_set_reconnect_delay(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "ctrl_loss_tmo")) { + libnvme_fabrics_config_set_ctrl_loss_tmo(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "fast_io_fail_tmo")) { + libnvme_fabrics_config_set_fast_io_fail_tmo(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "keep_alive_tmo")) { + libnvme_fabrics_config_set_keep_alive_tmo(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "nr_write_queues")) { + libnvme_fabrics_config_set_nr_write_queues(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "nr_poll_queues")) { + libnvme_fabrics_config_set_nr_poll_queues(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tos")) { + libnvme_fabrics_config_set_tos(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "keyring_id")) { + libnvme_fabrics_config_set_keyring_id(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_key_id")) { + libnvme_fabrics_config_set_tls_key_id(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_configured_key_id")) { + libnvme_fabrics_config_set_tls_configured_key_id(cfg, PyLong_AsLong(value)); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "duplicate_connect")) { + libnvme_fabrics_config_set_duplicate_connect(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "disable_sqflow")) { + libnvme_fabrics_config_set_disable_sqflow(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hdr_digest")) { + libnvme_fabrics_config_set_hdr_digest(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "data_digest")) { + libnvme_fabrics_config_set_data_digest(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls")) { + libnvme_fabrics_config_set_tls(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "concat")) { + libnvme_fabrics_config_set_concat(cfg, + PyObject_IsTrue(value) ? true : false); + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hostnqn")) { + hostnqn = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hostid")) { + hostid = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "hostkey")) { + hostkey = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "ctrlkey")) { + ctrlkey = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "keyring")) { + keyring = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_key")) { + tls_key = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "tls_key_identity")) { + tls_key_identity = (value != Py_None) ? PyUnicode_AsUTF8(value) : NULL; + continue; + } + if (!PyUnicode_CompareWithASCIIString(key, "persistent")) { + persistent = PyObject_IsTrue(value) ? true : false; + has_persistent = true; + continue; + } + /* Unknown keys are silently ignored */ + } + + if (hostnqn || hostid) + libnvmf_context_set_hostnqn(fctx, hostnqn, hostid); + if (hostkey || ctrlkey || keyring || tls_key || tls_key_identity) + libnvmf_context_set_crypto(fctx, hostkey, ctrlkey, + keyring, tls_key, + tls_key_identity); + if (has_persistent) + libnvmf_context_set_persistent(fctx, persistent); + + return 0; + } %} PyObject *read_hostnqn(); PyObject *read_hostid(); +/* libnvmf_context is declared as an opaque forward declaration in the public + * header fabrics.h. SWIG treats any type it knows only as an incomplete type + * (no struct body) as an opaque pointer, which causes SWIG's own built-in + * opaque-pointer typemap to take precedence over any user-defined %typemap. + * Providing an empty struct body here gives SWIG a complete type, which + * restores normal typemap precedence rules. The actual fields are in + * private-fabrics.h, which is included in the %{%} block above and is + * therefore visible to all generated C code — just not to SWIG's parser. + */ +struct libnvmf_context {}; + +/* Convert a Python dict to a struct libnvmf_context * automatically. + * arg1 is the libnvme_global_ctx * (first argument of the enclosing function). + * The context is created here and freed by %typemap(freearg) after the call. + */ +%typemap(in) struct libnvmf_context * (struct libnvmf_context *temp = NULL) { + if (!PyDict_Check($input)) { + PyErr_SetString(PyExc_TypeError, + "expected a dict for fabrics context argument"); + SWIG_fail; + } + if (libnvmf_context_create(arg1, NULL, NULL, NULL, NULL, &temp)) { + PyErr_SetString(PyExc_RuntimeError, + "failed to create fabrics context"); + SWIG_fail; + } + if (set_fctx_from_dict(temp, $input)) { + libnvmf_context_free(temp); + temp = NULL; + SWIG_fail; + } + $1 = temp; +} + +%typemap(freearg) struct libnvmf_context * { + libnvmf_context_free($1); +} + %exception libnvme_ctrl::connect { connect_err = 0; $action /* $action sets connect_err to non-zero value on failure */ @@ -432,113 +644,6 @@ struct libnvme_ns { uint8_t uuid[16]; }; -/* - * %rename directives give the %extend methods Python-friendly names while - * using distinct C-level names (fctx_*) that do not collide with the public - * libnvmf_context_* API declarations in fabrics.h. Without this, SWIG would - * emit SWIGINTERN libnvmf_context_set_hostnqn() which clashes with the - * non-static extern of the same name. - */ -%rename(set_hostnqn) libnvmf_context::fctx_set_hostnqn; -%rename(set_connection) libnvmf_context::fctx_set_connection; -%rename(set_persistent) libnvmf_context::fctx_set_persistent; -%rename(set_device) libnvmf_context::fctx_set_device; - -struct libnvmf_context {}; - -%extend libnvmf_context { - libnvmf_context(struct libnvme_global_ctx *ctx) { - struct libnvmf_context *fctx; - int err; - - err = libnvmf_context_create(ctx, NULL, NULL, NULL, NULL, &fctx); - if (err) - return NULL; - - return fctx; - } - ~libnvmf_context() { - libnvmf_context_free($self); - } - int fctx_set_hostnqn(const char *hostnqn, const char *hostid = NULL) { - return libnvmf_context_set_hostnqn($self, hostnqn, hostid); - } - int fctx_set_connection(const char *subsysnqn, const char *transport, - const char *traddr = NULL, const char *trsvcid = NULL, - const char *host_traddr = NULL, - const char *host_iface = NULL) { - return libnvmf_context_set_connection($self, subsysnqn, transport, - traddr, trsvcid, - host_traddr, host_iface); - } - int fctx_set_persistent(bool persistent) { - return libnvmf_context_set_persistent($self, persistent); - } - int fctx_set_device(const char *device) { - return libnvmf_context_set_device($self, device); - } - void fctx_set_fabrics_config(PyObject *dict) { - Py_ssize_t pos = 0; - PyObject *key, *value; - - if (!PyDict_Check(dict)) { - PyErr_SetString(PyExc_TypeError, - "set_fabrics_config: argument must be a dict"); - return; - } - - while (PyDict_Next(dict, &pos, &key, &value)) { - if (!PyUnicode_CompareWithASCIIString(key, "nr_io_queues")) { - $self->cfg.nr_io_queues = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "reconnect_delay")) { - $self->cfg.reconnect_delay = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "ctrl_loss_tmo")) { - $self->cfg.ctrl_loss_tmo = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "keep_alive_tmo")) { - $self->cfg.keep_alive_tmo = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "nr_write_queues")) { - $self->cfg.nr_write_queues = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "nr_poll_queues")) { - $self->cfg.nr_poll_queues = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "tos")) { - $self->cfg.tos = PyLong_AsLong(value); - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "duplicate_connect")) { - $self->cfg.duplicate_connect = - PyObject_IsTrue(value) ? true : false; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "disable_sqflow")) { - $self->cfg.disable_sqflow = - PyObject_IsTrue(value) ? true : false; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "hdr_digest")) { - $self->cfg.hdr_digest = - PyObject_IsTrue(value) ? true : false; - continue; - } - if (!PyUnicode_CompareWithASCIIString(key, "data_digest")) { - $self->cfg.data_digest = - PyObject_IsTrue(value) ? true : false; - continue; - } - } - } -}; %extend libnvme_global_ctx { libnvme_global_ctx(const char *config_file = NULL) { @@ -725,17 +830,69 @@ struct libnvmf_context {}; } %}; -%pythonappend libnvme_ctrl::connect(struct libnvme_host *h, - struct libnvmf_context *fctx) { +%pythonappend libnvme_ctrl::connect(struct libnvme_host *h) { self.__host = h # Keep a reference to parent to ensure ctrl obj gets GCed before host} %pythonappend libnvme_ctrl::init(struct libnvme_host *h, int instance) { self.__host = h # Keep a reference to parent to ensure ctrl obj gets GCed before host} %extend libnvme_ctrl { - libnvme_ctrl(struct libnvme_global_ctx *ctx, - struct libnvmf_context *fctx) { + %feature("autodoc", "__init__(ctrl self, global_ctx ctx, dict fctx) -> ctrl\n" + "\n" + "Create a new NVMe-oF controller object.\n" + "\n" + " params dict keys:\n" + "\n" + " Required:\n" + " subsysnqn (str) -- Subsystem NQN\n" + " transport (str) -- Transport type: 'tcp', 'rdma', 'loop', 'fc'\n" + "\n" + " Connection (optional):\n" + " traddr (str) -- Transport address\n" + " trsvcid (str) -- Service ID (port number)\n" + " host_traddr (str) -- Host transport address\n" + " host_iface (str) -- Host network interface\n" + "\n" + " Fabrics config (optional):\n" + " queue_size (int) -- IO queue entries\n" + " nr_io_queues (int) -- Number of IO queues\n" + " reconnect_delay (int) -- Reconnect interval in seconds\n" + " ctrl_loss_tmo (int) -- Controller loss timeout in seconds\n" + " fast_io_fail_tmo (int) -- Fast I/O fail timeout in seconds\n" + " keep_alive_tmo (int) -- Keep-alive timeout in seconds\n" + " nr_write_queues (int) -- Queues reserved for writes only\n" + " nr_poll_queues (int) -- Queues reserved for polling\n" + " tos (int) -- Type of service\n" + " keyring_id (int) -- Keyring ID for key lookup\n" + " tls_key_id (int) -- TLS PSK key ID\n" + " tls_configured_key_id (int) -- TLS PSK key ID for connect command\n" + " duplicate_connect (bool) -- Allow duplicate connections\n" + " disable_sqflow (bool) -- Disable SQ flow control\n" + " hdr_digest (bool) -- Header digest (TCP only)\n" + " data_digest (bool) -- Data digest (TCP only)\n" + " tls (bool) -- Enable TLS (TCP only)\n" + " concat (bool) -- Secure concatenation (TCP only)\n" + "\n" + " Host identity (optional):\n" + " hostnqn (str) -- Host NQN, overrides the system-wide default\n" + " hostid (str) -- Host ID, overrides the system-wide default\n" + "\n" + " Authentication and TLS (optional):\n" + " hostkey (str) -- Host DH-HMAC-CHAP key\n" + " ctrlkey (str) -- Controller DH-HMAC-CHAP key\n" + " keyring (str) -- Keyring identifier\n" + " tls_key (str) -- TLS key, or 'pin:' for PIN-derived key\n" + " tls_key_identity (str) -- TLS key identity string\n" + "\n" + " Persistence (optional):\n" + " persistent (bool) -- Keep connection alive after process exit\n" + ) libnvme_ctrl; + libnvme_ctrl(struct libnvme_global_ctx *ctx, struct libnvmf_context *fctx) { struct libnvme_ctrl *c; - if (libnvmf_create_ctrl(ctx, fctx, &c)) + + if (libnvmf_create_ctrl(ctx, fctx, &c)) { + PyErr_SetString(PyExc_RuntimeError, + "failed to create ctrl"); return NULL; + } return c; } ~libnvme_ctrl() { @@ -756,13 +913,6 @@ struct libnvmf_context {}; void connect(struct libnvme_host *h) { int ret; - const char *dev; - - dev = libnvme_ctrl_get_name($self); - if (dev && $self->cfg.duplicate_connect) { - connect_err = -ENVME_CONNECT_ALREADY; - return; - } Py_BEGIN_ALLOW_THREADS /* Release Python GIL */ ret = libnvmf_add_ctrl(h, $self); diff --git a/libnvme/libnvme/tests/create-ctrl-obj.py b/libnvme/libnvme/tests/create-ctrl-obj.py index 9112eae964..59a05f15c5 100755 --- a/libnvme/libnvme/tests/create-ctrl-obj.py +++ b/libnvme/libnvme/tests/create-ctrl-obj.py @@ -4,15 +4,12 @@ from libnvme import nvme -ctx = nvme.global_ctx() +ctx = nvme.global_ctx() ctx.log_level('debug') -fctx = nvme.fabrics_context(ctx) -fctx.set_connection( - subsysnqn=nvme.NVME_DISC_SUBSYS_NAME, - transport='loop', - traddr='127.0.0.1', - trsvcid='8009', -) - -ctrl = nvme.ctrl(ctx, fctx) +ctrl = nvme.ctrl(ctx, { + 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, + 'transport': 'loop', + 'traddr': '127.0.0.1', + 'trsvcid': '8009', +}) diff --git a/libnvme/libnvme/tests/gc.py b/libnvme/libnvme/tests/gc.py index c453fdec93..682074b9e8 100755 --- a/libnvme/libnvme/tests/gc.py +++ b/libnvme/libnvme/tests/gc.py @@ -12,16 +12,12 @@ host = nvme.host(ctx) print(f'host: {host}') -fctx = nvme.fabrics_context(ctx) -fctx.set_connection( - subsysnqn=nvme.NVME_DISC_SUBSYS_NAME, - transport='loop', -) -print(f'fctx: {fctx}') - ctrls = [] for i in range(10): - ctrl = nvme.ctrl(ctx, fctx) + ctrl = nvme.ctrl(ctx, { + 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, + 'transport': 'loop', + }) ctrls.append(ctrl) print(f'ctrl {i}: {ctrl}') diff --git a/libnvme/libnvme/tests/test-objects.py b/libnvme/libnvme/tests/test-objects.py index 9fafe0fe00..0fc2a885c4 100644 --- a/libnvme/libnvme/tests/test-objects.py +++ b/libnvme/libnvme/tests/test-objects.py @@ -118,26 +118,22 @@ def tearDown(self): gc.collect() def _make_loop_ctrl(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='loop' - ) - return nvme.ctrl(self.ctx, fctx) + return nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'loop', + }) def test_creation_loop_transport(self): ctrl = self._make_loop_ctrl() self.assertIsNotNone(ctrl) def test_creation_tcp_transport_with_traddr(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='tcp', - traddr='192.168.1.1', - trsvcid='4420' - ) - ctrl = nvme.ctrl(self.ctx, fctx) + ctrl = nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'tcp', + 'traddr': '192.168.1.1', + 'trsvcid': '4420', + }) self.assertIsNotNone(ctrl) def test_transport_property(self): @@ -149,24 +145,20 @@ def test_subsysnqn_property(self): self.assertEqual(ctrl.subsysnqn, self.subsysnqn) def test_traddr_property(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='tcp', - traddr='10.0.0.1', - ) - ctrl = nvme.ctrl(self.ctx, fctx) + ctrl = nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'tcp', + 'traddr': '10.0.0.1', + }) self.assertEqual(ctrl.traddr, '10.0.0.1') def test_trsvcid_property(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='tcp', - traddr='10.0.0.1', - trsvcid='8009', - ) - ctrl = nvme.ctrl(self.ctx, fctx) + ctrl = nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'tcp', + 'traddr': '10.0.0.1', + 'trsvcid': '8009', + }) self.assertEqual(ctrl.trsvcid, '8009') def test_connected_returns_false_before_connect(self): @@ -183,12 +175,10 @@ def test_str_contains_transport(self): self.assertIn('loop', s) def test_context_manager(self): - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=self.subsysnqn, - transport='loop' - ) - with nvme.ctrl(self.ctx, fctx) as c: + with nvme.ctrl(self.ctx, { + 'subsysnqn': self.subsysnqn, + 'transport': 'loop', + }) as c: self.assertIsNotNone(c) def test_namespaces_iterator_returns_list(self): @@ -234,12 +224,10 @@ class TestCtrlErrorHandling(unittest.TestCase): def setUp(self): self.ctx = nvme.global_ctx() - fctx = nvme.fabrics_context(self.ctx) - fctx.set_connection( - subsysnqn=nvme.NVME_DISC_SUBSYS_NAME, - transport='loop', - ) - self.ctrl = nvme.ctrl(self.ctx, fctx) + self.ctrl = nvme.ctrl(self.ctx, { + 'subsysnqn': nvme.NVME_DISC_SUBSYS_NAME, + 'transport': 'loop', + }) def tearDown(self): self.ctrl = None diff --git a/libnvme/src/nvme/fabrics.c b/libnvme/src/nvme/fabrics.c index 2512afa7b8..5d91a5b216 100644 --- a/libnvme/src/nvme/fabrics.c +++ b/libnvme/src/nvme/fabrics.c @@ -1102,6 +1102,10 @@ __public int libnvmf_add_ctrl(libnvme_host_t h, libnvme_ctrl_t c) __cleanup_free char *argstr = NULL; int ret; + /* Are duplicate connections allowed on existing controller */ + if (libnvme_ctrl_get_name(c) && !c->cfg.duplicate_connect) + return -ENVME_CONNECT_ALREADY; + /* apply configuration from config file (JSON) */ s = libnvme_lookup_subsystem(h, NULL, libnvme_ctrl_get_subsysnqn(c)); if (s) {