From fe78efe10d112e99b640d4ba860a0ea83a339b64 Mon Sep 17 00:00:00 2001 From: Hannes Reinecke Date: Fri, 5 Sep 2025 12:09:31 +0200 Subject: [PATCH 1/3] python: support 'with' statement for objects The python 'with' statement allows for correct resource tracking, where the allocated resource will be freed at the end of the statement. This is helpful for 'host', 'subsystem', and 'controller' objects which we might want to clean up (and clear out the internal tree). The nice thing here is that we can do an automatic controller disconnect, and don't have to worry about leaving stale connections or objects around. Signed-off-by: Hannes Reinecke --- libnvme/nvme.i | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/libnvme/nvme.i b/libnvme/nvme.i index 42deccfe5..9afa1d2bc 100644 --- a/libnvme/nvme.i +++ b/libnvme/nvme.i @@ -496,6 +496,12 @@ struct nvme_ns { ~nvme_root() { nvme_free_tree($self); } + struct nvme_root* __enter__() { + return $self; + } + struct nvme_root* __exit__(PyObject *type, PyObject *value, PyObject *traceback) { + return $self; + } void log_level(const char *level) { int log_level = DEFAULT_LOGLEVEL; if (!strcmp(level, "debug")) log_level = LOG_DEBUG; @@ -559,6 +565,12 @@ struct nvme_ns { ~nvme_host() { nvme_free_host($self); } + struct nvme_host* __enter__() { + return $self; + } + struct nvme_host* __exit__(PyObject *type, PyObject *value, PyObject *traceback) { + return $self; + } %feature("autodoc", SET_SYMNAME_DOCSTRING) set_symname; void set_symname(const char *hostsymname) { nvme_host_set_hostsymname($self, hostsymname); @@ -599,6 +611,12 @@ struct nvme_ns { ~nvme_subsystem() { nvme_free_subsystem($self); } + struct nvme_subsystem* __enter__() { + return $self; + } + struct nvme_subsystem* __exit__(PyObject *type, PyObject *value, PyObject *traceback) { + return $self; + } PyObject *__str__() { return PyUnicode_FromFormat("nvme.subsystem(%s,%s)", STR_OR_NONE($self->name), STR_OR_NONE($self->subsysnqn)); } @@ -664,6 +682,14 @@ struct nvme_ns { ~nvme_ctrl() { nvme_free_ctrl($self); } + struct nvme_ctrl* __enter__() { + return $self; + } + struct nvme_ctrl* __exit__(PyObject *type, PyObject *value, PyObject *traceback) { + if (nvme_ctrl_get_name($self)) + nvme_disconnect_ctrl($self); + return $self; + } %pythoncode %{ def discovery_ctrl_set(self, discovery: bool): @@ -962,6 +988,12 @@ struct nvme_ns { ~nvme_ns() { nvme_free_ns($self); } + struct nvme_ns* __enter__() { + return $self; + } + struct nvme_ns* __exit__(PyObject *type, PyObject *value, PyObject *traceback) { + return $self; + } PyObject *__str__() { return PyUnicode_FromFormat("nvme.ns(%u)", $self->nsid); } From 2828dcb560bb9620d89cbd4485e2ccb9a9c242cd Mon Sep 17 00:00:00 2001 From: Hannes Reinecke Date: Fri, 5 Sep 2025 12:18:44 +0200 Subject: [PATCH 2/3] examples/discover-loop.py: rework to use 'with' statement Now that we have resource tracking we can use the 'with' statement for discovery and clean up connections and discovery controllers on exit. Signed-off-by: Hannes Reinecke --- examples/discover-loop.py | 61 +++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/examples/discover-loop.py b/examples/discover-loop.py index 944d8dd4a..b5011d7c9 100644 --- a/examples/discover-loop.py +++ b/examples/discover-loop.py @@ -16,46 +16,43 @@ def disc_supp_str(dlp_supp_opts): } return [txt for msk, txt in d.items() if dlp_supp_opts & msk] -root = nvme.root() -host = nvme.host(root) - -subsysnqn = nvme.NVME_DISC_SUBSYS_NAME -transport = 'tcp' -traddr = '127.0.0.1' -trsvcid = '4420' +def discover(host, ctrl): + try: + ctrl.connect(host) + except Exception as e: + print(f'Failed to connect: {e}') + return -ctrl = nvme.ctrl(root, subsysnqn=subsysnqn, transport=transport, traddr=traddr, trsvcid=trsvcid) + print(f'{ctrl.name} connected to subsys {ctrl.subsystem}') -try: - ctrl.connect(host) -except Exception as e: - sys.exit(f'Failed to connect: {e}') + slp = ctrl.supported_log_pages() + try: + dlp_supp_opts = slp[nvme.NVME_LOG_LID_DISCOVER] >> 16 + except (TypeError, IndexError): + dlp_supp_opts = 0 -print(f'{ctrl.name} connected to subsys {ctrl.subsystem}') + print(f"LID {nvme.NVME_LOG_LID_DISCOVER}h (Discovery), supports: {disc_supp_str(dlp_supp_opts)}") -slp = ctrl.supported_log_pages() + try: + lsp = nvme.NVMF_LOG_DISC_LSP_PLEO if dlp_supp_opts & nvme.NVMF_LOG_DISC_LID_PLEOS else 0 + disc_log = ctrl.discover(lsp=lsp) + except Exception as e: + print(f'Failed to discover: {e}') + return -try: - dlp_supp_opts = slp[nvme.NVME_LOG_LID_DISCOVER] >> 16 -except (TypeError, IndexError): - dlp_supp_opts = 0 + for dlpe in disc_log: + print(f'log entry {dlpe["portid"]}: {dlpe["subtype"]} {dlpe["subnqn"]}') -print(f"LID {nvme.NVME_LOG_LID_DISCOVER}h (Discovery), supports: {disc_supp_str(dlp_supp_opts)}") - -try: - lsp = nvme.NVMF_LOG_DISC_LSP_PLEO if dlp_supp_opts & nvme.NVMF_LOG_DISC_LID_PLEOS else 0 - disc_log = ctrl.discover(lsp=lsp) -except Exception as e: - print(f'Failed to discover: {e}') - disc_log = [] +root = nvme.root() +host = nvme.host(root) -for dlpe in disc_log: - print(f'log entry {dlpe["portid"]}: {dlpe["subtype"]} {dlpe["subnqn"]}') +subsysnqn = nvme.NVME_DISC_SUBSYS_NAME +transport = 'tcp' +traddr = '127.0.0.1' +trsvcid = '4420' -try: - ctrl.disconnect() -except Exception as e: - sys.exit(f'Failed to disconnect: {e}') +with nvme.ctrl(root, subsysnqn=subsysnqn, transport=transport, traddr=traddr, trsvcid=trsvcid) as ctrl: + discover(host, ctrl) for s in host.subsystems(): for c in s.controllers(): From d30c72e26ecf77ab48de336a3df3ccc1da6e415c Mon Sep 17 00:00:00 2001 From: Hannes Reinecke Date: Fri, 5 Sep 2025 13:17:17 +0200 Subject: [PATCH 3/3] examples/discover-loop.py: actually implement a discovery loop As we now have resource tracking we can implement a discovery loop in python without leaving stale controllers around. Signed-off-by: Hannes Reinecke --- examples/discover-loop.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/discover-loop.py b/examples/discover-loop.py index b5011d7c9..741f2dcf8 100644 --- a/examples/discover-loop.py +++ b/examples/discover-loop.py @@ -16,14 +16,18 @@ def disc_supp_str(dlp_supp_opts): } return [txt for msk, txt in d.items() if dlp_supp_opts & msk] -def discover(host, ctrl): +def discover(host, ctrl, iteration): + # Only 8 levels of indirection are supported + if iteration > 8: + return + try: ctrl.connect(host) except Exception as e: print(f'Failed to connect: {e}') return - print(f'{ctrl.name} connected to subsys {ctrl.subsystem}') + print(f'{ctrl.name} connected to {ctrl.subsystem}') slp = ctrl.supported_log_pages() try: @@ -41,7 +45,14 @@ def discover(host, ctrl): return for dlpe in disc_log: - print(f'log entry {dlpe["portid"]}: {dlpe["subtype"]} {dlpe["subnqn"]}') + if dlpe['subtype'] == 'nvme': + print(f'{iteration}: {dlpe["subtype"]} {dlpe["subnqn"]}') + continue + if dlpe['subtype'] == 'discovery' and dlpe['subnqn'] == nvme.NVME_DISC_SUBSYS_NAME: + continue + print(f'{iteration}: {dlpe["subtype"]} {dlpe["subnqn"]}') + with nvme.ctrl(root, subsysnqn=dlpe['subnqn'], transport=dlpe['trtype'], traddr=dlpe['traddr'], trsvcid=dlpe['trsvcid']) as new_ctrl: + discover(host, new_ctrl, iteration + 1) root = nvme.root() host = nvme.host(root) @@ -52,7 +63,7 @@ def discover(host, ctrl): trsvcid = '4420' with nvme.ctrl(root, subsysnqn=subsysnqn, transport=transport, traddr=traddr, trsvcid=trsvcid) as ctrl: - discover(host, ctrl) + discover(host, ctrl, 0) for s in host.subsystems(): for c in s.controllers():