Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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__)"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ build/*
dist/*
*.pyc
*.pyc
.coverage

# generated documentation
Documentation/*.1
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
4 changes: 2 additions & 2 deletions README
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------------------------------
Expand Down Expand Up @@ -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 .
Expand Down
4 changes: 2 additions & 2 deletions bump-ver.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ Source: nvmetcli
Section: python
Priority: optional
Maintainer: Christoph Hellwig <hch@lst.de>
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
Expand Down
1 change: 0 additions & 1 deletion debian/pyversions

This file was deleted.

6 changes: 3 additions & 3 deletions debian/rules
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 8 additions & 28 deletions nvmet/nvme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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

Expand Down
73 changes: 58 additions & 15 deletions nvmet/test_nvmet.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import random
import stat
import string
import tempfile
import unittest
from nvmet import nvme

Expand All @@ -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):
Expand Down Expand Up @@ -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()

Expand All @@ -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())

Expand All @@ -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')

Expand Down Expand Up @@ -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()

Expand All @@ -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')
Expand Down Expand Up @@ -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')
Expand Down
26 changes: 26 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[build-system]
requires = ["setuptools>=77.0.3"]
build-backend = "setuptools.build_meta"
Comment thread
igaw marked this conversation as resolved.

[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"
]
8 changes: 4 additions & 4 deletions rpm/nvmetcli.spec.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading
Loading