Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a39b12e
Add NVMfService
bmeagherix May 28, 2025
eb955af
Add SPDK plumbing
bmeagherix May 28, 2025
d6409ca
Add python3-truenas-spdk as build/runtime depend
bmeagherix Jun 2, 2025
8a36963
Add private API system.cpu_flags
bmeagherix Jun 5, 2025
0c6c006
Do not allow SPDK without avx2 in CPU flags
bmeagherix Jun 5, 2025
1054355
Add SPDK support for nvmet.host_subsys, including CHAP keys
bmeagherix Jun 6, 2025
62dda68
Handle min/max controller id
bmeagherix Jun 6, 2025
645d804
Refactor some code into utils/nvmet directory
bmeagherix Jun 9, 2025
a997d60
Update nvmet.spdk.nics for HA
bmeagherix Jun 11, 2025
9bf6a61
Initial HA implementation for SPDK
bmeagherix Jun 12, 2025
924503d
Remove NULL bdev when BACKUP node
bmeagherix Jun 13, 2025
919f0be
Add NvmetPortAnaReferralConfig
bmeagherix Jun 17, 2025
4df0e6b
Add implementation of nvmet.global.sessions for SPDK backend
bmeagherix Jun 17, 2025
9ded544
Make setup.sh honor PCI_ALLOWED
bmeagherix Jun 24, 2025
783ed4f
Add NVMETargetService.failure_logs
bmeagherix Jul 15, 2025
829726b
For FILE namespace force the block_size to underlying recordsize
bmeagherix Jul 16, 2025
92fe35b
Do not attempt to dedicate NICs for SPDK use
bmeagherix Oct 13, 2025
097089b
Replace service.start/stop with service.control
bmeagherix Oct 13, 2025
442bb34
Remove incorrect max_cntlid code in NvmetHostSubsysConfig.add
bmeagherix Oct 14, 2025
2f4e40f
Add missing await in nvmet.global.stop
bmeagherix Oct 14, 2025
98a1940
Support ZVOLs containing a space in SPDK-based nvmet
bmeagherix Oct 14, 2025
c821295
Add missing namespace lock/unlock/resize support for SPDK
bmeagherix Oct 15, 2025
6906171
Always report 512-byte blocksize for SPDK file-based extents
bmeagherix Oct 15, 2025
f5767c7
In _handle_standby_service_state wait for remote operation
bmeagherix Oct 16, 2025
3bf5060
Run NVMe-oF tests for both kernel and SPDK implementations
bmeagherix Oct 16, 2025
9a23032
Make test__file_namespaces reentrant
bmeagherix Oct 16, 2025
d1a17df
Run test__start_many_nvme for both kernel and SPDK
bmeagherix Oct 17, 2025
b86dac4
Address review
bmeagherix Oct 17, 2025
3444915
Robustize local_sessions
bmeagherix Oct 17, 2025
cd7ba8b
Address review: simplify system.cpu_flags
bmeagherix Oct 17, 2025
faa8ea3
Address review
bmeagherix Oct 17, 2025
2ab6461
Address review: remove unnecessary if before os.makedirs
bmeagherix Oct 17, 2025
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
2 changes: 2 additions & 0 deletions src/middlewared/debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Build-Depends: alembic,
python3-truenas-pylibvirt,
python3-truenas-pynetif,
python3-truenas-pyscstadmin,
python3-truenas-spdk,
python3-truenas-verify,
python3-websocket,
python3-zeroconf,
Expand Down Expand Up @@ -162,6 +163,7 @@ Depends: alembic,
python3-truenas-pylibvirt,
python3-truenas-pynetif,
python3-truenas-pyscstadmin,
python3-truenas-spdk,
python3-truenas-verify,
python3-websocket,
python3-zeroconf,
Expand Down
2 changes: 1 addition & 1 deletion src/middlewared/middlewared/etc_files/nvmet_kernel.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from middlewared.plugins.nvmet.kernel import write_config
from middlewared.utils.nvmet.kernel import write_config


def render(service, middleware, render_ctx):
Expand Down
7 changes: 7 additions & 0 deletions src/middlewared/middlewared/etc_files/nvmet_spdk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from middlewared.utils.nvmet.spdk import inject_path_to_recordsize, write_config


def render(service, middleware, render_ctx):
if middleware.call_sync('nvmet.spdk.nvmf_ready', True):
inject_path_to_recordsize(middleware, render_ctx)
write_config(render_ctx)
1 change: 1 addition & 0 deletions src/middlewared/middlewared/plugins/etc.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ class EtcService(Service):
],
'entries': [
{'type': 'py', 'path': 'nvmet_kernel'},
{'type': 'py', 'path': 'nvmet_spdk'},
]
},
'pam': {
Expand Down
16 changes: 9 additions & 7 deletions src/middlewared/middlewared/plugins/nvmet/constants.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import enum

NVMET_KERNEL_CONFIG_DIR = '/sys/kernel/config/nvmet'
NVMET_NODE_A_ANA_GRPID = 2
NVMET_NODE_B_ANA_GRPID = 3

NVMET_DISCOVERY_NQN = 'nqn.2014-08.org.nvmexpress.discovery'
NVMET_NQN_UUID = 'nqn.2011-06.com.truenas:uuid'
NVMET_SERVICE_NAME = 'nvmet'

Expand All @@ -24,6 +22,10 @@ def api(self):
def sysfs(self):
return self.value[2]

@property
def spdk(self):
return self.value[3]

@classmethod
def by_db(cls, needle, raise_exception=True):
for x in cls.__members__.values():
Expand Down Expand Up @@ -68,10 +70,10 @@ class PORT_TRTYPE(ApiMapper):


class PORT_ADDR_FAMILY(ApiMapper):
IPV4 = (1, 'IPV4', 'ipv4')
IPV6 = (2, 'IPV6', 'ipv6')
IB = (3, 'IB', 'ib')
FC = (4, 'FC', 'fc')
IPV4 = (1, 'IPV4', 'ipv4', 'IPv4')
IPV6 = (2, 'IPV6', 'ipv6', 'IPv6')
IB = (3, 'IB', 'ib', 'IB')
FC = (4, 'FC', 'fc', 'FC')


def port_transport_family_generator():
Expand Down
120 changes: 93 additions & 27 deletions src/middlewared/middlewared/plugins/nvmet/global.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@
from middlewared.service import SystemServiceService, ValidationErrors, filterable_api_method, private
from middlewared.utils import filter_list
from .constants import NVMET_SERVICE_NAME
from .kernel import clear_config, load_modules, nvmet_kernel_module_loaded, unload_module
from middlewared.utils.nvmet.kernel import clear_config, load_modules, nvmet_kernel_module_loaded, unload_module
from middlewared.utils.nvmet.spdk import make_client, nvmf_subsystem_get_qpairs
from .mixin import NVMetStandbyMixin
from .utils import uuid_nqn

NVMET_DEBUG_DIR = '/sys/kernel/debug/nvmet'
NVMF_SERVICE = 'nvmf'
AVX2_FLAG = 'avx2'


class NVMetGlobalModel(sa.Model):
Expand Down Expand Up @@ -65,9 +68,11 @@ async def do_update(self, data):
async def __ana_forbidden(self):
return not await self.middleware.call('failover.licensed')

async def __spdk_forbidden(self):
# For now we'll disallow SPDK if any CPU is missing avx2
return any(AVX2_FLAG not in flags for flags in (await self.middleware.call('system.cpu_flags')).values())

async def __validate(self, verrors, data, schema_name, old=None):
if not data.get('kernel', False):
verrors.add(f'{schema_name}.kernel', 'Cannot disable kernel mode.')
if data['rdma'] and old['rdma'] != data['rdma']:
available_rdma_protocols = await self.middleware.call('rdma.capable_protocols')
if RDMAprotocols.NVMET.value not in available_rdma_protocols:
Expand All @@ -81,6 +86,17 @@ async def __validate(self, verrors, data, schema_name, old=None):
f'{schema_name}.ana',
'This platform does not support Asymmetric Namespace Access(ANA).'
)
if old['kernel'] != data['kernel']:
if not data['kernel'] and await self.__spdk_forbidden():
verrors.add(
f'{schema_name}.kernel',
'Cannot switch nvmet backend because CPU lacks required capabilities.'
)
elif await self.running():
verrors.add(
f'{schema_name}.kernel',
'Cannot switch nvmet backend while the service is running.'
)

@private
async def ana_enabled(self):
Expand Down Expand Up @@ -121,9 +137,9 @@ async def rdma_enabled(self):
async def sessions(self, filters, options):
sessions = []
subsys_id = None
for filter in filters:
if len(filter) == 3 and filter[0] == 'subsys_id' and filter[1] == '=':
subsys_id = filter[2]
for _filter in filters:
if len(_filter) == 3 and _filter[0] == 'subsys_id' and _filter[1] == '=':
subsys_id = _filter[2]
break
sessions = await self.middleware.call('nvmet.global.local_sessions', subsys_id)
if await self.ana_enabled():
Expand Down Expand Up @@ -159,33 +175,74 @@ def local_sessions(self, subsys_id=None):
sessions = []
global_info = self.middleware.call_sync('nvmet.global.config')
subsystems = self.middleware.call_sync('nvmet.subsys.query')
ports = self.middleware.call_sync('nvmet.port.query')
ha = self.middleware.call_sync('failover.licensed')

if global_info['kernel']:
nvmet_debug_path = pathlib.Path(NVMET_DEBUG_DIR)
if not nvmet_debug_path.exists():
return sessions

port_index_to_id = {port['index']: port['id'] for port in self.middleware.call_sync('nvmet.port.query')}
port_index_to_id = {port['index']: port['id'] for port in ports}

if subsys_id is None:
basenqn = global_info['basenqn']
subsys_name_to_subsys_id = {f'{basenqn}:{subsys["name"]}': subsys['id'] for subsys in subsystems}
for subsys in nvmet_debug_path.iterdir():
if subsys_id := subsys_name_to_subsys_id.get(subsys.name):
for ctrl in subsys.iterdir():
if session := self.__parse_session_dir(ctrl, port_index_to_id):
session['subsys_id'] = subsys_id
sessions.append(session)
else:
for subsys in subsystems:
if subsys['id'] == subsys_id:
subnqn = f'{global_info["basenqn"]}:{subsys["name"]}'
path = nvmet_debug_path / subnqn
if path.is_dir():
for ctrl in path.iterdir():
if subsys_id is None:
subsys_name_to_subsys_id = {subsys['subnqn']: subsys['id'] for subsys in subsystems}
for subsys in nvmet_debug_path.iterdir():
if subsys_id := subsys_name_to_subsys_id.get(subsys.name):
for ctrl in subsys.iterdir():
if session := self.__parse_session_dir(ctrl, port_index_to_id):
session['subsys_id'] = subsys_id
sessions.append(session)
else:
for subsys in subsystems:
if subsys['id'] == subsys_id:
subnqn = subsys['subnqn']
path = nvmet_debug_path / subnqn
if path.is_dir():
for ctrl in path.iterdir():
if session := self.__parse_session_dir(ctrl, port_index_to_id):
session['subsys_id'] = subsys_id
sessions.append(session)
else:
if not self.middleware.call_sync('nvmet.spdk.nvmf_ready'):
return sessions
client = make_client()
choices = {}
port_to_portid = {}
for port in ports:
addr = port['addr_traddr']
trtype = port['addr_trtype']
addrs = [addr]
if ha:
try:
mychoices = choices[trtype]
except KeyError:
mychoices = self.middleware.call_sync('nvmet.port.transport_address_choices', trtype, True)
mychoices[trtype] = mychoices
if pair := mychoices.get(addr, '').split('/'):
addrs.extend(pair)

for addr in addrs:
port_to_portid[f"{trtype}:{addr}:{port['addr_trsvcid']}"] = port['id']
for subsys in subsystems:
if subsys_id is not None and subsys_id != subsys['id']:
continue
for entry in nvmf_subsystem_get_qpairs(client, subsys['subnqn']):
laddr = entry['listen_address']
key = f"{laddr['trtype']}:{laddr['traddr']}:{laddr['trsvcid']}"
if port_id := port_to_portid.get(key):
session = {
'host_traddr': entry.get('peer_address', {}).get('traddr'),
'hostnqn': entry['hostnqn'],
'subsys_id': subsys['id'],
'port_id': port_id,
'ctrl': entry['cntlid']
}
# Do we really care that we might have multiple duplicate connections
# (only visible delta being a different qid and peer_address.trsvcid)
# If so, replace this with a simple append.
if session not in sessions:
sessions.append(session)

return sessions

Expand Down Expand Up @@ -216,7 +273,7 @@ async def running(self):
if (await self.config())['kernel']:
return await self.middleware.run_in_thread(nvmet_kernel_module_loaded)
else:
return False
return await self.middleware.call('service.started', NVMF_SERVICE)

@private
async def reload(self):
Expand All @@ -225,14 +282,23 @@ async def reload(self):

@private
async def start(self):
await self.middleware.call('nvmet.global.load_kernel_modules')
if (await self.config())['kernel']:
await self.middleware.call('nvmet.global.load_kernel_modules')
else:
await self.middleware.call('nvmet.spdk.slots')
if await (await self.middleware.call('service.control', 'START', NVMF_SERVICE)).wait(raise_error=True):
await self.middleware.call('nvmet.spdk.wait_nvmf_ready')
await self.middleware.call('etc.generate', 'nvmet')

@private
async def stop(self):
if await self.running():
await self.middleware.run_in_thread(clear_config)
await self.middleware.call('nvmet.global.unload_kernel_modules')
if (await self.config())['kernel']:
await self.middleware.run_in_thread(clear_config)
await self.middleware.call('nvmet.global.unload_kernel_modules')
else:
job = await self.middleware.call('service.control', 'STOP', NVMF_SERVICE)
return await job.wait(raise_error=True)

@private
async def system_ready(self):
Expand Down
8 changes: 6 additions & 2 deletions src/middlewared/middlewared/plugins/nvmet/mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ async def _handle_standby_service_state(self, check=False):
if new_state:
# Start on STANDBY node
await self.middleware.call('failover.call_remote',
'service.control', ['START', NVMET_SERVICE_NAME, {'ha_propagate': False}])
'service.control',
['START', NVMET_SERVICE_NAME, {'ha_propagate': False}],
{'job': True})
else:
# Stop on STANDBY node
await self.middleware.call('failover.call_remote',
'service.control', ['STOP', NVMET_SERVICE_NAME, {'ha_propagate': False}])
'service.control',
['STOP', NVMET_SERVICE_NAME, {'ha_propagate': False}],
{'job': True})
26 changes: 23 additions & 3 deletions src/middlewared/middlewared/plugins/nvmet/namespace.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
from middlewared.service import SharingService, ValidationErrors, private
from middlewared.service_exception import CallError, MatchNotFound
from .constants import NAMESPACE_DEVICE_TYPE
from .kernel import lock_namespace as kernel_lock_namespace
from .kernel import unlock_namespace as kernel_unlock_namespace
from .kernel import resize_namespace as kernel_resize_namespace
from middlewared.utils.nvmet.kernel import lock_namespace as kernel_lock_namespace
from middlewared.utils.nvmet.kernel import unlock_namespace as kernel_unlock_namespace
from middlewared.utils.nvmet.kernel import resize_namespace as kernel_resize_namespace
from middlewared.utils.nvmet.spdk import lock_namespace as spdk_lock_namespace
from middlewared.utils.nvmet.spdk import unlock_namespace as spdk_unlock_namespace
from middlewared.utils.nvmet.spdk import resize_namespace as spdk_resize_namespace

UUID_GENERATE_RETRIES = 10
NSID_SEARCH_RANGE = 0xFFFF # This is much less than NSID, but good enough for practical purposes.
Expand Down Expand Up @@ -361,20 +364,37 @@ async def stop(self, id_):
if data['enabled']:
if (await self.middleware.call('nvmet.global.config'))['kernel']:
await self.middleware.run_in_thread(kernel_lock_namespace, data)
else:
render_ctx = {}
for api in ['nvmet.namespace.query', 'failover.status']:
render_ctx[api] = await self.middleware.call(api)
await self.middleware.run_in_thread(spdk_lock_namespace, data, render_ctx)

@private
async def start(self, id_):
data = await self.get_instance(id_)
if data['enabled'] and await self.middleware.call('failover.status') in ('MASTER', 'SINGLE'):
if (await self.middleware.call('nvmet.global.config'))['kernel']:
await self.middleware.run_in_thread(kernel_unlock_namespace, data)
else:
render_ctx = {}
for api in ['nvmet.namespace.query',
'failover.node',
'failover.status']:
render_ctx[api] = await self.middleware.call(api)
await self.middleware.run_in_thread(spdk_unlock_namespace, self.middleware, data, render_ctx)

@private
async def resize_namespace(self, id_):
data = await self.get_instance(id_)
if data['enabled'] and await self.middleware.call('failover.status') in ('MASTER', 'SINGLE'):
if (await self.middleware.call('nvmet.global.config'))['kernel']:
await self.middleware.run_in_thread(kernel_resize_namespace, data)
else:
render_ctx = {}
for api in ['failover.status']:
render_ctx[api] = await self.middleware.call(api)
await self.middleware.run_in_thread(spdk_resize_namespace, data, render_ctx)

@private
async def sharing_task_determine_locked(self, data):
Expand Down
Loading
Loading