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__)" 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 6a7beb1..fcf7ca1 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 ----------------------------------------- @@ -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/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..b26ed05 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 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/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/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') diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c0f429b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[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", + "coverage" +] diff --git a/rpm/nvmetcli.spec.tmpl b/rpm/nvmetcli.spec.tmpl index ce454a2..48c600f 100644 --- a/rpm/nvmetcli.spec.tmpl +++ b/rpm/nvmetcli.spec.tmpl @@ -8,8 +8,8 @@ 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 -Requires: python-configshell python-kmod +BuildRequires: python3-devel python3-pip python3-build systemd-units +Requires: python-configshell kmod Requires(post): systemd Requires(preun): systemd Requires(postun): 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'] - )