From 0752ba472b405d9b3b2ffcc6be1ce7a2885b7a44 Mon Sep 17 00:00:00 2001 From: Ales Novak Date: Tue, 14 Jul 2026 17:18:53 +0200 Subject: [PATCH 1/5] build: replace setup.py with PEP 517/518 tooling Update debian/rules, rpm/nvmetcli.spec.tmpl, README, and bump-ver.sh to use python3 -m build / pip3 install instead of setup.py, since setup.py has been replaced by pyproject.toml. Signed-off-by: Ales Novak Signed-off-by: Daniel Wagner --- README | 2 +- bump-ver.sh | 4 ++-- debian/control | 4 ++-- debian/pyversions | 1 - debian/rules | 6 +++--- pyproject.toml | 25 +++++++++++++++++++++++++ rpm/nvmetcli.spec.tmpl | 6 +++--- setup.py | 31 ------------------------------- 8 files changed, 36 insertions(+), 43 deletions(-) delete mode 100644 debian/pyversions create mode 100644 pyproject.toml delete mode 100755 setup.py diff --git a/README b/README index 6a7beb1..a8e520e 100644 --- a/README +++ b/README @@ -10,7 +10,7 @@ Please install the configshell-fb package from https://github.com/open-iscsi/configshell-fb first. nvmetcli can be run directly from the source directory or installed -using setup.py. +using pip install . Common Package Dependencies and Problems ----------------------------------------- diff --git a/bump-ver.sh b/bump-ver.sh index 21f7df3..232d84c 100755 --- a/bump-ver.sh +++ b/bump-ver.sh @@ -15,8 +15,8 @@ if [ -z "$VER" ]; then fi make clean -sed -i "s/version =.*,/version = $VER,/" setup.py -git add setup.py +sed -i "s/^version = .*/version = \"$VER\"/" pyproject.toml +git add pyproject.toml git commit -m "bump version to v$VER" git tag -s "v$VER" -m "nvmetcli release v$VER" diff --git a/debian/control b/debian/control index 9ba8d0e..b6f7660 100644 --- a/debian/control +++ b/debian/control @@ -2,12 +2,12 @@ Source: nvmetcli Section: python Priority: optional Maintainer: Christoph Hellwig -Build-Depends: debhelper(>= 8), python, python-setuptools, dh-python, dh-systemd (>= 1.5) +Build-Depends: debhelper(>= 8), python3, python3-setuptools, python3-pip, python3-build, dh-python, dh-systemd (>= 1.5) Standards-Version: 3.9.4 Package: nvmetcli Architecture: all -Depends: ${misc:Depends}, ${python:Depends}, python-configshell-fb, python-kmodpy +Depends: ${misc:Depends}, ${python3:Depends}, python3-configshell-fb, python-kmodpy Description: Command line interface for the kernel NVMe target This package contains the command line interface to the NVMe over Fabrics target in the Linux kernel. It allows configuring the target interactively diff --git a/debian/pyversions b/debian/pyversions deleted file mode 100644 index 3ad2293..0000000 --- a/debian/pyversions +++ /dev/null @@ -1 +0,0 @@ -2.7- diff --git a/debian/rules b/debian/rules index 1869a24..708dc09 100755 --- a/debian/rules +++ b/debian/rules @@ -8,13 +8,13 @@ export http_proxy = http://127.0.0.1:9 %: - dh $@ --with python2 --with systemd + dh $@ --with python3 --with systemd override_dh_auto_build: - python setup.py build + python3 -m build override_dh_auto_install: - python setup.py install --root=$(install_dir) --install-layout=deb + python3 -m pip install --no-deps --no-build-isolation --root=$(install_dir) --prefix=/usr . override_dh_auto_clean: dh_auto_clean diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..70858bc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["setuptools>=77.0.3"] +build-backend = "setuptools.build_meta" + +[project] +name = "nvmetcli" +version = "0.8" +description = "NVMe target configuration tool" +dependencies = ["configshell-fb>=2.0"] +license = "Apache-2.0" +license-files = ["LICENSE"] +requires-python = ">=3.10" +maintainers = [ + { name = "Christoph Hellwig", email = "hch@lst.de" }, + { name = "Daniel Wagner", email = "wagi@monom.org" } +] + +[tool.setuptools] +packages = ["nvmet"] +script-files = ["nvmetcli"] + +[project.optional-dependencies] +test = [ + "nose2" +] diff --git a/rpm/nvmetcli.spec.tmpl b/rpm/nvmetcli.spec.tmpl index ce454a2..69dd3dc 100644 --- a/rpm/nvmetcli.spec.tmpl +++ b/rpm/nvmetcli.spec.tmpl @@ -8,7 +8,7 @@ URL: http://git.infradead.org/users/hch/nvmetcli.git Source: nvmetcli-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-rpmroot BuildArch: noarch -BuildRequires: python-devel python-setuptools systemd-units +BuildRequires: python3-devel python3-pip python3-build systemd-units Requires: python-configshell python-kmod Requires(post): systemd Requires(preun): systemd @@ -23,11 +23,11 @@ as well as saving / restoring the configuration to / from a json file. %setup -q -n nvmetcli-%{version} %build -%{__python} setup.py build +%{__python3} -m build %install rm -rf %{buildroot} -%{__python} setup.py install --skip-build --root=%{buildroot} --prefix=usr +%{__python3} -m pip install --no-deps --no-build-isolation --root=%{buildroot} --prefix=/usr . mkdir -p %{buildroot}%{_sysconfdir}/nvmet mkdir -p %{buildroot}%{_unitdir} install -m 644 nvmet.service %{buildroot}%{_unitdir}/nvmet.service diff --git a/setup.py b/setup.py deleted file mode 100755 index 2b202e8..0000000 --- a/setup.py +++ /dev/null @@ -1,31 +0,0 @@ -#! /usr/bin/env python -''' -This file is part of ConfigShell. -Copyright (c) 2011-2013 by Datera, Inc - -Licensed under the Apache License, Version 2.0 (the "License"); you may -not use this file except in compliance with the License. You may obtain -a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -License for the specific language governing permissions and limitations -under the License. -''' - -from setuptools import setup - -setup( - name = 'nvmetcli', - version = 0.8, - description = 'NVMe target configuration tool', - license = 'Apache 2.0', - maintainer = 'Christoph Hellwig', - maintainer_email = 'hch@lst.de', - test_suite='nose2.collector.collector', - packages = ['nvmet'], - scripts=['nvmetcli'] - ) From 6238684683f579d868963f441e9bb5c70a215a35 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Tue, 18 Aug 2026 18:55:25 +0200 Subject: [PATCH 2/5] build: add workflow to test build Verify that building the project with the PEP 517/518 tooling works. Signed-off-by: Daniel Wagner --- .github/workflows/build.yml | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c4d7519 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,54 @@ +--- +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (c) 2026 SUSE Software Solutions + +name: Build + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + build: + name: Build sdist & wheel (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Keep in sync with https://devguide.python.org/versions/ + # (all currently-supported CPython releases). + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: ${{ matrix.python-version }} + + # setup.cfg's [install] section (needed by debian/rules and the rpm + # spec to place nvmetcli under /usr/sbin) is picked up by pip/distutils + # from the current working directory on older interpreters, and would + # otherwise hijack the install location of unrelated tooling below. + # Run all "pip install" steps outside the checkout to avoid that. + - name: Install build frontend + working-directory: ${{ runner.temp }} + run: python3 -m pip install --upgrade build twine + + - name: Build sdist and wheel + run: python3 -m build + + - name: Check package metadata + run: python3 -m twine check dist/* + + - name: Install built wheel + working-directory: ${{ runner.temp }} + run: python3 -m pip install "${GITHUB_WORKSPACE}"/dist/*.whl + + - name: Import smoke test + working-directory: ${{ runner.temp }} + run: python3 -c "import nvmet; print(nvmet.__file__)" From a0f663bce8fb0be01a158ffc0f5ef9b2f832856c Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Tue, 18 Aug 2026 19:18:21 +0200 Subject: [PATCH 3/5] nvmetcli: remove python-kmod The python-kmod project has been abandoned for a while. Remove the dependency and just use the 'modprobe' approach. Signed-off-by: Daniel Wagner --- debian/control | 2 +- nvmet/nvme.py | 36 ++++++++---------------------------- rpm/nvmetcli.spec.tmpl | 2 +- 3 files changed, 10 insertions(+), 30 deletions(-) diff --git a/debian/control b/debian/control index b6f7660..b26ed05 100644 --- a/debian/control +++ b/debian/control @@ -7,7 +7,7 @@ Standards-Version: 3.9.4 Package: nvmetcli Architecture: all -Depends: ${misc:Depends}, ${python3:Depends}, python3-configshell-fb, python-kmodpy +Depends: ${misc:Depends}, ${python3:Depends}, python3-configshell-fb Description: Command line interface for the kernel NVMe target This package contains the command line interface to the NVMe over Fabrics target in the Linux kernel. It allows configuring the target interactively diff --git a/nvmet/nvme.py b/nvmet/nvme.py index 5899c75..ecd26af 100644 --- a/nvmet/nvme.py +++ b/nvmet/nvme.py @@ -26,14 +26,6 @@ import shlex from doctest import testmod from glob import iglob as glob -try: - from kmodpy import kmod -except ImportError: - kmod = None -try: - import kmod as kmod_ctypes -except ImportError: - kmod_ctypes = None DEFAULT_SAVE_FILE = '/etc/nvmet/config.json' @@ -293,28 +285,16 @@ def _modprobe(self, modname): ''' Load a kernel module. ''' - if kmod: - try: - kmod.Kmod().modprobe(modname, quiet=True) - return - except kmod.KmodError: - pass - - if kmod_ctypes: - try: - kmod_ctypes.Kmod().modprobe(modname) - return - except OSError: - pass - - # Try the binary specified in /proc + # Use the binary specified in /proc, falling back to "modprobe" + modprobe_cmd = 'modprobe' try: - modprobe_cmd = None with open('/proc/sys/kernel/modprobe', 'r', encoding="utf-8") as f: - modprobe_cmd = f.read() - if modprobe_cmd: - subprocess.run(shlex.split(modprobe_cmd) + [modname], - check=False) + modprobe_cmd = f.read().strip() or modprobe_cmd + except OSError: + pass + + try: + subprocess.run(shlex.split(modprobe_cmd) + [modname], check=False) except OSError: pass diff --git a/rpm/nvmetcli.spec.tmpl b/rpm/nvmetcli.spec.tmpl index 69dd3dc..48c600f 100644 --- a/rpm/nvmetcli.spec.tmpl +++ b/rpm/nvmetcli.spec.tmpl @@ -9,7 +9,7 @@ Source: nvmetcli-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-rpmroot BuildArch: noarch BuildRequires: python3-devel python3-pip python3-build systemd-units -Requires: python-configshell python-kmod +Requires: python-configshell kmod Requires(post): systemd Requires(preun): systemd Requires(postun): systemd From 5208a3139eb450e3acd45e03a3cb6dbbb1dcf4d4 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Tue, 18 Aug 2026 19:53:17 +0200 Subject: [PATCH 4/5] build: use 'coverage run nose' Support for the nose2 coverage plugin is gradually being removed. Signed-off-by: Daniel Wagner --- .gitignore | 1 + Makefile | 2 +- README | 2 +- pyproject.toml | 3 ++- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 5a8946e..7f82d9f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ build/* dist/* *.pyc *.pyc +.coverage # generated documentation Documentation/*.1 diff --git a/Makefile b/Makefile index 57ff7e4..b288b56 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ all: @echo " make uninstalldoc - Uninstall man pages (need sudo)." test: - @python3 -m nose2 -C --coverage ./nvmet + @coverage run --source=nvmet -m nose2; ret=$$?; coverage report; exit $$ret doc: ${NAME} ${MAKE} -C ${DOCDIR} diff --git a/README b/README index a8e520e..fcf7ca1 100644 --- a/README +++ b/README @@ -51,7 +51,7 @@ Testing ------- nvmetcli comes with a testsuite that tests itself and the kernel configfs interface for the NVMe target. To run it make sure you have nose2 and -the coverage plugin for it installed and simple run 'make test'. To run all +coverage installed and simple run 'make test'. To run all the tests you also need some test block devices or files. Default is to use /dev/ram0 and /dev/ram1. You can override default with environmental variable eg. NVMET_TEST_DEVICES="/dev/sdk,/dev/sdj" make test . diff --git a/pyproject.toml b/pyproject.toml index 70858bc..c0f429b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,5 +21,6 @@ script-files = ["nvmetcli"] [project.optional-dependencies] test = [ - "nose2" + "nose2", + "coverage" ] From a07e3e576930d7fa1d3a42619ae0ac01dd7e7534 Mon Sep 17 00:00:00 2001 From: Daniel Wagner Date: Tue, 18 Aug 2026 20:18:30 +0200 Subject: [PATCH 5/5] test_nvmet: fall back to temp sparse files as backing devices Previously test_namespace_attrs and test_save_restore were skipped outright when NVMET_TEST_DEVICES (default /dev/ram0,/dev/ram1) were not present. Add get_test_devices(), which prefers configured/present devices but falls back to creating 512M sparse temp files for any that are missing. Signed-off-by: Daniel Wagner --- nvmet/test_nvmet.py | 73 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/nvmet/test_nvmet.py b/nvmet/test_nvmet.py index 4fd1c4c..7c8900a 100644 --- a/nvmet/test_nvmet.py +++ b/nvmet/test_nvmet.py @@ -6,6 +6,7 @@ import random import stat import string +import tempfile import unittest from nvmet import nvme @@ -14,14 +15,58 @@ NVMET_TEST_DEVICES = os.getenv("NVMET_TEST_DEVICES", "/dev/ram0,/dev/ram1").split(',') +# Size used for the sparse backing files created when a test needs more +# backing devices than are configured/present. +TEMP_BACKING_FILE_SIZE = 512 * 1024 * 1024 -def test_devices_present(): + +def _is_usable_device(path): + ''' + Return True if 'path' is usable as a namespace backing device (a + block device or a regular file). Any OSError from stat() (missing + path, permission denied, races, ...) is treated as "not usable" + rather than propagated. + ''' + try: + st = os.stat(path) + except OSError: + return False + return stat.S_ISBLK(st.st_mode) or stat.S_ISREG(st.st_mode) + + +def _usable_devices(): + ''' + Return the subset of NVMET_TEST_DEVICES that are usable as a + namespace backing device. + ''' + return [x for x in NVMET_TEST_DEVICES if _is_usable_device(x)] + + +def _make_temp_backing_file(): + ''' + Create a temporary sparse file suitable for use as a namespace + backing file, equivalent to 'truncate --size=512M'. + ''' + fd, path = tempfile.mkstemp(prefix='nvmet-test-ns-', suffix='.img') + os.close(fd) + os.truncate(path, TEMP_BACKING_FILE_SIZE) + return path + + +def get_test_devices(count, testcase): ''' - Check if the test devices are present. + Return a list of 'count' backing devices to use for namespaces. + + Configured/present devices (see NVMET_TEST_DEVICES) are preferred; + any additional devices needed are created as temporary sparse files + and scheduled for removal via testcase.addCleanup(). ''' - return len([x for x in NVMET_TEST_DEVICES - if os.path.exists(x) and - (stat.S_ISBLK(os.stat(x).st_mode) or os.path.isfile(x))]) >= 2 + devices = _usable_devices() + while len(devices) < count: + path = _make_temp_backing_file() + testcase.addCleanup(os.remove, path) + devices.append(path) + return devices class TestNvmet(unittest.TestCase): @@ -129,13 +174,12 @@ def test_namespace(self): n.delete() self.assertEqual(len(list(s.namespaces)), 0) - @unittest.skipUnless(test_devices_present(), - f"Devices {','.join(NVMET_TEST_DEVICES)} " - f"not available or suitable") def test_namespace_attrs(self): ''' Test Namespace attributes. ''' + devices = get_test_devices(2, self) + root = nvme.Root() root.clear_existing() @@ -150,7 +194,7 @@ def test_namespace_attrs(self): self.assertRaises(nvme.CFSError, n.set_enable, 1) # now set a path and enable - n.set_attr('device', 'path', NVMET_TEST_DEVICES[0]) + n.set_attr('device', 'path', devices[0]) n.set_enable(1) self.assertTrue(n.get_enable()) @@ -159,7 +203,7 @@ def test_namespace_attrs(self): # test that we can't write to attrs while enabled self.assertRaises(nvme.CFSError, n.set_attr, 'device', 'path', - NVMET_TEST_DEVICES[1]) + devices[1]) self.assertRaises(nvme.CFSError, n.set_attr, 'device', 'nguid', '15f7767b-50e7-4441-949c-75b99153dea7') @@ -458,13 +502,12 @@ def test_invalid_input(self): self.assertRaises(nvme.CFSError, nvme.Port, portid=1 << 17, mode='create') - @unittest.skipUnless(test_devices_present(), - f"Devices {','.join(NVMET_TEST_DEVICES)} not " - f"available or suitable") def test_save_restore(self): ''' Test save and restore functionality. ''' + devices = get_test_devices(1, self) + root = nvme.Root() root.clear_existing() @@ -478,7 +521,7 @@ def test_save_restore(self): s2.set_attr('attr', 'allow_any_host', 1) n = nvme.Namespace(s, nsid=42, mode='create') - n.set_attr('device', 'path', NVMET_TEST_DEVICES[0]) + n.set_attr('device', 'path', devices[0]) n.set_enable(1) nguid = n.get_attr('device', 'nguid') @@ -516,7 +559,7 @@ def test_save_restore(self): # and check everything is still the same self.assertTrue(n.get_enable()) - self.assertEqual(n.get_attr('device', 'path'), NVMET_TEST_DEVICES[0]) + self.assertEqual(n.get_attr('device', 'path'), devices[0]) self.assertEqual(n.get_attr('device', 'nguid'), nguid) self.assertEqual(h.get_attr('dhchap', 'dhgroup'), 'ffdhe2048')