Skip to content

Commit 828bd95

Browse files
author
Martin Belanger
committed
python bindings: add exception hierarchy
Replace the connect_err/discover_err globals and %exception shims with a proper exception hierarchy in libnvme/_exceptions.py: NvmeError (base, .errno + .message) ├── ConnectError ├── DisconnectError ├── DiscoverError └── NotConnectedError (.errno is always 0) Exceptions are imported into the nvme module namespace at init time via %init %{} and re-exported via %pythoncode so callers can use nvme.ConnectError etc. directly. raise_nvme(cls, err) sets the exception from C; raise_not_connected() handles the no-errno case. SWIG_fail cannot be used inside %extend function bodies (they are extracted into SWIGINTERN helpers where the fail: label is absent), so minimal %exception blocks propagate any pending exception set by the helper back into the wrapper. Also rename supported_log_pages() to get_supported_log_pages(), which now raises NvmeError on failure instead of returning None. Update discover-loop.py and README.md to match the new API. Signed-off-by: Martin Belanger <[email protected]> Assisted-by: Claude Sonnet 4.6 <[email protected]>
1 parent 28edbd0 commit 828bd95

6 files changed

Lines changed: 302 additions & 110 deletions

File tree

libnvme/examples/discover-loop.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,24 +23,24 @@ def discover(ctx, host, ctrl, iteration):
2323

2424
try:
2525
ctrl.connect(host)
26-
except Exception as e:
26+
except nvme.ConnectError as e:
2727
print(f'Failed to connect: {e}')
2828
return
2929

3030
print(f'{ctrl.name} connected to {ctrl.subsystem}')
3131

32-
slp = ctrl.supported_log_pages()
3332
try:
33+
slp = ctrl.get_supported_log_pages()
3434
dlp_supp_opts = slp[nvme.NVME_LOG_LID_DISCOVERY] >> 16
35-
except (TypeError, IndexError):
35+
except (nvme.NvmeError, IndexError, TypeError):
3636
dlp_supp_opts = 0
3737

3838
print(f"LID {nvme.NVME_LOG_LID_DISCOVERY}h (Discovery), supports: {disc_supp_str(dlp_supp_opts)}")
3939

4040
try:
4141
lsp = nvme.NVMF_LOG_DISC_LSP_PLEO if dlp_supp_opts & nvme.NVMF_LOG_DISC_LID_PLEOS else 0
4242
disc_log = ctrl.discover(lsp=lsp)
43-
except Exception as e:
43+
except nvme.DiscoverError as e:
4444
print(f'Failed to discover: {e}')
4545
return
4646

@@ -52,7 +52,7 @@ def discover(ctx, host, ctrl, iteration):
5252
continue
5353
print(f'{iteration}: {dlpe["subtype"]} {dlpe["subnqn"]}')
5454
with nvme.Ctrl(ctx, subsysnqn=dlpe['subnqn'], transport=dlpe['trtype'], traddr=dlpe['traddr'], trsvcid=dlpe['trsvcid']) as new_ctrl:
55-
discover(host, new_ctrl, iteration + 1)
55+
discover(ctx, host, new_ctrl, iteration + 1)
5656

5757
ctx = nvme.GlobalCtx()
5858
host = nvme.Host(ctx)

libnvme/libnvme/README.md

Lines changed: 125 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,84 @@
33

44
We use [SWIG](http://www.swig.org/) to generate Python bindings for libnvme.
55

6+
## Classes
7+
8+
Five classes are exposed, matching the NVMe topology:
9+
10+
| Class | Description |
11+
|---|---|
12+
| `nvme.GlobalCtx` | Root context. Manages the NVMe device tree and logging. Create one instance per process. |
13+
| `nvme.Host` | Represents the local host: NQN, host ID, optional symbolic name (symname). |
14+
| `nvme.Subsystem` | An NVMe subsystem discovered under a host. |
15+
| `nvme.Ctrl` | An NVMe controller (physical or fabrics). Constructed from a configuration dict. |
16+
| `nvme.Namespace` | A namespace under a controller. |
17+
18+
All five classes support the context manager protocol (`with` statement) for automatic cleanup.
19+
20+
Topology can be traversed with iterators: `ctx.hosts()`, `host.subsystems()`,
21+
`subsystem.controllers()`, `ctrl.namespaces()`.
22+
23+
### Ctrl configuration dict
24+
25+
`nvme.Ctrl(ctx, cfg)` accepts a flat dict. Common keys:
26+
27+
| Key | Description |
28+
|---|---|
29+
| `subsysnqn` | Subsystem NQN (required) |
30+
| `transport` | Transport type: `pcie`, `tcp`, `rdma`, `fc`, `loop`, `apple-nvme` (required) |
31+
| `traddr` | Target address |
32+
| `trsvcid` | Target service ID (port) |
33+
| `host_iface` | Local network interface to use |
34+
| `hostnqn` | Override the host NQN |
35+
| `hostid` | Override the host ID |
36+
| `hdr_digest` | Enable header digests (`bool`) |
37+
| `data_digest` | Enable data digests (`bool`) |
38+
| `persistent` | Keep the connection alive after the object is released (`bool`) |
39+
40+
### Key Ctrl methods and properties
41+
42+
| Name | Type | Description |
43+
|---|---|---|
44+
| `connected` | property | `True` if the controller is currently connected |
45+
| `registration_supported` | property | `True` if the target supports explicit host registration |
46+
| `name` | property | Kernel device name (e.g. `nvme0`), or `None` if not connected |
47+
| `transport`, `traddr`, `trsvcid` | properties | Connection parameters |
48+
| `connect(host)` | method | Establish the kernel connection |
49+
| `disconnect()` | method | Tear down the connection |
50+
| `discover()` | method | Retrieve the discovery log page (discovery controllers only) |
51+
| `get_supported_log_pages()` | method | Fetch the Supported Log Pages log |
52+
| `rescan()` | method | Rescan the controller and refresh its namespace list |
53+
| `registration_control(tas)` | method | Register / deregister / update with the DIM service |
54+
55+
## Exceptions
56+
57+
All libnvme errors are reported through a small exception hierarchy:
58+
59+
```
60+
NvmeError base class — carries .errno (int) and .message (str)
61+
├── ConnectError raised by ctrl.connect()
62+
├── DisconnectError raised by ctrl.disconnect()
63+
├── DiscoverError raised by ctrl.discover()
64+
└── NotConnectedError raised when an operation requires a connected controller
65+
(.errno is always 0)
66+
```
67+
68+
Import them directly from the `nvme` module:
69+
70+
```python
71+
from libnvme import nvme
72+
73+
try:
74+
ctrl.connect(host)
75+
except nvme.ConnectError as e:
76+
print(f"errno={e.errno}, message={e.message}")
77+
except nvme.NotConnectedError:
78+
print("not connected")
79+
```
80+
81+
`NvmeError` is also raised by `get_supported_log_pages()` and
82+
`registration_control()` on failure.
83+
684
## How to use
785

886
```python
@@ -19,59 +97,75 @@ def disc_supp_str(dlp_supp_opts):
1997
}
2098
return [txt for msk, txt in bitmap.items() if dlp_supp_opts & msk]
2199

22-
ctx = nvme.global_ctx() # This is a singleton
23-
ctx.log_level('debug') # Optional: extra debug info
100+
ctx = nvme.GlobalCtx()
101+
ctx.log_level('debug') # Optional: extra debug info
24102

25-
host = nvme.host(ctx) # This "may be" a singleton.
26-
subsysnqn = [string] # e.g. nvme.NVME_DISC_SUBSYS_NAME, ...
27-
transport = [string] # One of: 'tcp', 'rdma', 'fc', 'loop'.
28-
traddr = [IPv4 or IPv6] # e.g. '192.168.10.10', 'fd2e:853b:3cad:e135:506a:65ee:29f2:1b18', ...
29-
trsvcid = [string] # e.g. '8009', '4420', ...
30-
host_iface = [interface] # e.g. 'eth1', ens256', ...
31-
ctrl = nvme.ctrl(ctx, subsysnqn=subsysnqn, transport=transport, traddr=traddr, trsvcid=trsvcid, host_iface=host_iface)
103+
host = nvme.Host(ctx)
104+
105+
ctrl = nvme.Ctrl(ctx, {
106+
'subsysnqn': '...', # e.g. nvme.NVME_DISC_SUBSYS_NAME
107+
'transport': '...', # One of: 'tcp', 'rdma', 'fc', 'loop'
108+
'traddr': '...', # e.g. '192.168.10.10'
109+
'trsvcid': '...', # e.g. '8009', '4420'
110+
'host_iface': '...', # e.g. 'eth1', 'ens256'
111+
'hdr_digest': True, # Enable header digests
112+
'data_digest': False, # Disable data digests
113+
})
32114

33115
try:
34-
cfg = {
35-
'hdr_digest': True, # Enable header digests
36-
'data_digest': False, # Disable data digests
37-
}
38-
ctrl.connect(host, cfg)
116+
ctrl.connect(host)
39117
print(f"connected to {ctrl.name} subsys {ctrl.subsystem.name}")
40-
except Exception as e:
118+
except nvme.ConnectError as e:
41119
sys.exit(f'Failed to connect: {e}')
42120

43-
supported_log_pages = ctrl.supported_log_pages()
44121
try:
45-
# Get the supported options for the Get Discovery Log Page command
46-
dlp_supp_opts = supported_log_pages[nvme.NVME_LOG_LID_DISCOVERY] >> 16
47-
except (TypeError, IndexError):
122+
slp = ctrl.get_supported_log_pages()
123+
dlp_supp_opts = slp[nvme.NVME_LOG_LID_DISCOVERY] >> 16
124+
except (nvme.NvmeError, IndexError, TypeError):
48125
dlp_supp_opts = 0
49126

50127
print(f"LID {nvme.NVME_LOG_LID_DISCOVERY:02x}h (Discovery), supports: {disc_supp_str(dlp_supp_opts)}")
128+
51129
try:
52130
lsp = nvme.NVMF_LOG_DISC_LSP_PLEO if dlp_supp_opts & nvme.NVMF_LOG_DISC_LID_PLEOS else 0
53131
log_pages = ctrl.discover(lsp=lsp)
54132
print(pprint.pformat(log_pages))
55-
except Exception as e:
133+
except nvme.DiscoverError as e:
56134
sys.exit(f'Failed to retrieve log pages: {e}')
57135

58136
try:
59137
ctrl.disconnect()
60-
except Exception as e:
138+
except nvme.DisconnectError as e:
61139
sys.exit(f'Failed to disconnect: {e}')
62-
63-
ctrl = None
64-
host = None
65-
ctx = None
66140
```
67141

68-
## Testing PyPI package
142+
## Installation
69143

70-
Use the following command to test installing the package from TestPyPI before publishing to the official PyPI registry.
144+
The package is available from most Linux distribution repositories and on PyPI.
145+
146+
**From your distribution (recommended):**
71147

72148
```bash
73-
pip install \
74-
--index-url https://test.pypi.org/simple/ \
75-
--extra-index-url https://pypi.org/simple/ \
76-
libnvme==[libnvme-version]
149+
# Debian / Ubuntu
150+
apt-get install python3-libnvme
151+
152+
# Fedora
153+
dnf install python3-libnvme
154+
155+
# openSUSE
156+
zypper install python3-libnvme
77157
```
158+
159+
**From PyPI:**
160+
161+
```bash
162+
pip install libnvme
163+
```
164+
165+
> **Note:** The PyPI package is a source distribution — it builds libnvme and
166+
> the Python bindings directly on your machine. Build dependencies (a C
167+
> compiler, Meson, Ninja, SWIG, and the libnvme C dependencies) must be
168+
> present. If any are missing, `pip install` will fail. Installing from your
169+
> distribution package manager is generally easier and avoids this requirement.
170+
171+
See [PUBLISHING.md](PUBLISHING.md) for instructions on testing and publishing new releases.

libnvme/libnvme/_exceptions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# SPDX-License-Identifier: LGPL-2.1-or-later
2+
# This file is part of libnvme.
3+
# Copyright (c) 2026, Dell Technologies Inc. or its subsidiaries.
4+
# Authors: Martin Belanger <[email protected]>
5+
6+
7+
class NvmeError(Exception):
8+
"""Base class for all libnvme errors.
9+
10+
Attributes:
11+
errno: OS error number (negative values are stored as-is).
12+
message: Human-readable description from libnvme_errno_to_string().
13+
"""
14+
def __init__(self, errno, message):
15+
self.errno = errno
16+
self.message = message
17+
super().__init__(f"[Errno {errno}] {message}")
18+
19+
20+
class ConnectError(NvmeError):
21+
"""Raised when a controller connection attempt fails."""
22+
23+
24+
class DisconnectError(NvmeError):
25+
"""Raised when a controller disconnect attempt fails."""
26+
27+
28+
class DiscoverError(NvmeError):
29+
"""Raised when a discovery log retrieval fails."""
30+
31+
32+
class NotConnectedError(NvmeError):
33+
"""Raised when an operation requires a connected controller but none exists."""
34+
def __init__(self, message="Not connected"):
35+
super().__init__(0, message)

libnvme/libnvme/meson.build

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,22 @@ if want_python
6060
},
6161
)
6262

63-
# Little hack to copy file __init__.py to the build directory.
64-
# This is needed to create the proper directory layout to run the tests.
65-
# It's a hack because we don't really "configure" file __init__.py and we
66-
# could simply install directly from the source tree with:
67-
# python3.install_sources(['__init__.py', ], pure:false, subdir:'libnvme')
68-
# However, since we need __init__.py in the build directory to run the tests
69-
# we resort to this hack to copy it.
70-
configure_file(
71-
input: '__init__.py',
72-
output: '__init__.py',
73-
configuration: conf,
74-
install_dir: python3.get_install_dir(pure: false, subdir: 'libnvme'),
75-
)
63+
# Little hack to copy a list of files [__init__.py, etc.] to the build
64+
# directory. This is needed to create the proper directory layout to run the
65+
# tests. It's a hack because we don't really "configure" files and we could
66+
# simply install directly from the source tree with:
67+
# python3.install_sources(files, pure:false, subdir:'libnvme')
68+
# However, since we need these files in the build directory to run the tests
69+
# we resort to this hack to copy them.
70+
files = ['__init__.py', '_exceptions.py']
71+
foreach file: files
72+
configure_file(
73+
input: file,
74+
output: file,
75+
configuration: conf,
76+
install_dir: python3.get_install_dir(pure: false, subdir: 'libnvme'),
77+
)
78+
endforeach
7679

7780
# Set the PYTHONPATH so that we can run the
7881
# tests directly from the build directory.

0 commit comments

Comments
 (0)